spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1# CancerIndex — Public API23Base URL: `https://www.cancerindex.io/api/v1` (proxied by the web app to the Fastify service on4`http://127.0.0.1:8251/v1`). Read-only. JSON only.56**The reference documentation is generated from the route schemas and is always current:**78- Swagger UI: `/v1/docs`9- OpenAPI 3.1: `/v1/openapi.json`1011This page only explains the conventions and gives examples. Do not edit endpoint descriptions here;12change the zod schemas in `apps/api/src/routes/*.ts` (CLAUDE.md §357).1314## Conventions1516### Envelope1718Every `/v1` response is wrapped:1920```json21{22 "data": …,23 "sources": [{ "id": "CI-SOURCE-00000001", "slug": "oncotree", "name": "OncoTree", "license": "CC BY 4.0", "attribution": "OncoTree (MSKCC), CC BY 4.0, https://oncotree.mskcc.org", "url": "https://oncotree.mskcc.org" }],24 "dataRelease": "CancerIndex 2026-09",25 "generatedAt": "2026-09-08T09:33:19.433Z",26 "total": 865, "limit": 50, "offset": 0, "hasMore": true27}28```2930`sources` are the distinct upstream sources behind `data` (license and attribution included so you31can comply with redistribution terms). Pagination fields appear on list endpoints; `limit` ≤ 200.3233### Identifiers3435Entities are addressed by their public id (`CI-CAN-00000364`) or a human reference: cancer slug36(`lung-adenocarcinoma`), HGNC symbol (`BRAF`, aliases accepted), variant slug, drug slug, `NCT` id,37PMID. Merged cancers redirect to the surviving entity.3839### Errors4041```json42{ "error": { "code": "not_found", "message": "cancer \"foo\" not found" }, "requestId": "…" }43```4445Codes: `bad_request` (400, with zod `details`), `unauthorized` (401), `not_found` (404),46`rate_limited` (429), `service_unavailable` (503), `internal` (500). Every response carries47`x-request-id`; quote it when reporting a problem.4849### Rate limits and API keys5051Anonymous: 60 requests/minute per IP. With `Authorization: Bearer <key>`: the key's own limit52(`api_keys.rate_limit_per_minute`). Headers: `x-ratelimit-limit`, `x-ratelimit-remaining`,53`x-ratelimit-reset`, `retry-after` on 429. Mint a key:5455```bash56pnpm --filter @cancerindex/api create-key -- --label "Lab X" --email lab@example.org --tier research --rpm 60057```5859Only the sha256 hash and a prefix are stored; the key is printed once.6061### Scientific labels6263Payload objects that carry a number also carry the category (`observed_data`, `curated_evidence`,64`regulatory_status`, `computed_metric`) and a `provenance` object (source, dataset, version,65`retrievedAt`, URL). Missing data is `null` or an empty list — never `0` substituted for "unknown".66`counters: null` means counters have not been computed yet.6768## Endpoints (summary)6970| Method + path | Purpose |71|---|---|72| `GET /healthz` | `{ ok, dataRelease, db }` |73| `GET /v1/cancers` | list; `q`, `level=top|all`, `type`, `malignant`, `hematologic`, `pediatric`, `rare`, `sort=name|active_trials|publications_5y`, `limit`, `offset` |74| `GET /v1/cancers/:id` | entity + aliases + codes + hierarchy (parents/children per hierarchy type, breadcrumbs) + anatomy + counters + completeness + current rankings + last 20 changes |75| `GET /v1/cancers/:id/statistics` | epidemiology observations with per-row provenance + charting `series`; filters `metric`, `geography`, `sex` |76| `GET /v1/cancers/:id/survival` | survival observations with provenance |77| `GET /v1/cancers/:id/genes` | cohort frequencies (denominators) + curated evidence per gene, cancer + descendants |78| `GET /v1/cancers/:id/variants` | variants with evidence counts by level/direction/type |79| `GET /v1/cancers/:id/drugs` | evidence by drug (level/direction/significance) + jurisdiction-aware approvals |80| `GET /v1/cancers/:id/trials` | trials for the cancer and descendants; `status`, `phase`, `interventionalOnly` |81| `GET /v1/cancers/:id/publications` | publication edges + literature count windows (query stored) |82| `GET /v1/genes` · `GET /v1/genes/:symbol` | genes; detail with cancers (curated + cohort), variants, drugs, literature |83| `GET /v1/variants/:id` | variant, ClinVar interpretations, CIViC evidence grouped by cancer, edges |84| `GET /v1/drugs` · `GET /v1/drugs/:id` | drugs; detail with evidence by cancer, approvals, trials |85| `GET /v1/trials` · `GET /v1/trials/:nct` | trial search (`q`, `status`, `phase`, `cancer`, `country`); detail with condition/intervention mappings, locations (200), publications |86| `GET /v1/publications/:pmid` | publication + entity edges (abstract truncated, links) |87| `GET /v1/rankings/metrics` | metric catalog with available scopes |88| `GET /v1/rankings` | `metric`, `geography`, `sex`, `age`, `year`, `level`; snapshot metadata + metric definition + rows |89| `GET /v1/rankings/:metric/:cancerId/explain` | "Why this rank?": inputs, previous rank, neighbours, lineage trace |90| `GET /v1/search?q=` | cross-entity search, ≤ 20 typed results, exact > alias > prefix > fuzzy |91| `GET /v1/sources` · `GET /v1/sources/:slug` | registry, license status, connector health, runs, counts |92| `GET /v1/stats` | live counts (60 s cache) |93| `GET /v1/changes` | change events; `entityType`, `entityId`, `kind`, `since` |94| `GET /v1/epidemiology` · `/epidemiology/coverage` · `/epidemiology/metrics` | time-aware observations with provenance (`metric`, `cancer` ≤ 8, `geography`, `sex`, `age`, `from`, `to`, `source`, `estimateType`); coverage matrix; metrics present |95| `GET /v1/trials/intelligence` · `/trials/intelligence/:cancer` | derived per-cancer trial metrics (`level=top|all`, `sort`, `order`, `minActive`): counts, growth YoY, enrollment, sponsor/country HHI, termination share, trials per 1,000 deaths — formula `ci-trial-intel-v1` |96| `GET /v1/trials/terminated` | terminated / withdrawn / suspended studies with registrant-stated reason and keyword category (`cancer`, `reason`, `status`, `since`) |97| `GET /v1/trials/sites` | country (or city) aggregates of trial sites (`cancer` top-level, `phase`, `recruiting`, `level=country|city`) |98| `GET /v1/research-gap` · `/research-gap/scopes` | Research Gap components per burden scope (`geography`, `year`, `sex`, `source`): deaths, trials, publications, shares, log₂ ratios, per-1,000-deaths intensities |99| `GET /v1/approvals` · `/approvals/recent` | jurisdiction-aware approval records (`authority`, `jurisdiction`, `cancer`, `drug`, `status`, `from`, `to`, `q`); recent feed grouped by month |100| `GET /v1/pipeline` · `/pipeline/summary` | drug development stage per drug / per drug × top-level cancer (`cancer`, `stage`, `drug`) — formula `ci-drug-pipeline-v1` |101| `GET /v1/graph/:type/:id` · `/graph/cancer/:id/paths` | knowledge-graph neighbourhood (source-native edges + derived registry links; `limit`, `rel`, `context`, `includeDerived`) and cancer → gene → variant → drug → approval → trial chains |102| `GET/POST /v1/admin/*` | operators only (`x-admin-token`): connectors, run/pause/resume, runs/:runId, unresolved + resolve, trace, jobs/counters, jobs/rank, audit |103104## Examples105106```bash107API=http://127.0.0.1:8251108109# health110curl -s $API/healthz111112# homepage ticker113curl -s $API/v1/stats | jq .data114115# cancers whose name/alias starts with "glio", sorted by active trials116curl -s "$API/v1/cancers?q=glio&sort=active_trials&limit=5" | jq '.data[] | {id, slug, name, counters}'117118# one cancer with hierarchy and current rankings119curl -s $API/v1/cancers/lung-adenocarcinoma | jq '.data | {name, primaryNcitCode, hierarchy: .hierarchy.parents, rankings}'120121# epidemiology observations with provenance for a top-level cancer, US only122curl -s "$API/v1/cancers/lung-cancer/statistics?geography=USA&sex=all" | jq '.data.series[0]'123124# trials recruiting for a cancer and its subtypes125curl -s "$API/v1/cancers/lung-adenocarcinoma/trials?status=RECRUITING&limit=10" | jq '.data[] | {nctId, briefTitle, phases}'126127# a gene and its cancers128curl -s $API/v1/genes/BRAF | jq '.data | {symbol, counters, cancers: .cancers.curatedEvidence[0:3]}'129130# a variant: evidence separated by cancer131curl -s $API/v1/variants/braf-v600e | jq '.data.evidenceByCancer[] | {cancer: .cancer.name, acceptedItems}'132133# ranking: active trials, top-level set134curl -s "$API/v1/rankings?metric=active_trials&level=top&limit=10" | jq '{snapshot: .data.snapshot, top: [.data.rows[] | {rank, name: .cancer.name, value}]}'135136# why this rank? (lineage down to raw records)137curl -s "$API/v1/rankings/active_trials/lung-cancer/explain?level=top" | jq '.data | {rank, previousRank, inputs, trace}'138139# search140curl -s "$API/v1/search?q=glio" | jq '.data[] | {type, name, match}'141142# sources and license status143curl -s $API/v1/sources | jq '.data[] | {slug, licenseStatus, status, health: .connector.health, records: .counts.sourceRecords}'144145# authenticated request (higher limit)146curl -s -H "Authorization: Bearer cix_…" $API/v1/stats -D - -o /dev/null | grep -i x-ratelimit147148# admin: enqueue a connector run, then watch it149curl -s -X POST -H "x-admin-token: $ADMIN_TOKEN" -H 'content-type: application/json' \150 -d '{"mode":"incremental","maxMinutes":30,"reason":"manual refresh"}' $API/v1/admin/connectors/clinicaltrials/run151curl -s -H "x-admin-token: $ADMIN_TOKEN" $API/v1/admin/connectors | jq '.data[] | {connectorId, health, lastSuccessAt, recentRuns: .recentRuns[0].status}'152153# admin: curation queue and resolution154curl -s -H "x-admin-token: $ADMIN_TOKEN" "$API/v1/admin/unresolved?entityKind=cancer&limit=20"155curl -s -X POST -H "x-admin-token: $ADMIN_TOKEN" -H 'content-type: application/json' \156 -d '{"cancerId":"CI-CAN-00000364","reason":"synonym per NCIt"}' $API/v1/admin/unresolved/123/resolve157```158159Resolving an unresolved label adds a curated `cancer_aliases` row (`alias_type = synonym`,160`source_terminology = curation`, `source_id` = the label's source), re-maps `trial_conditions` rows161that carry the same normalised text, writes `audit_log` and a `change_events` row; every other162connector picks the alias up on its next run through `CancerResolver`.163164## Local development165166```bash167pnpm dev:api # tsx watch, http://127.0.0.1:8251168pnpm --filter @cancerindex/api test # unit tests + read-only smoke tests (skipped if DB unreachable)169pnpm --filter @cancerindex/api typecheck170```171