SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
11 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
9.9 KB

# CancerIndex — Public API

Base URL: https://www.cancerindex.io/api/v1 (proxied by the web app to the Fastify service on http://127.0.0.1:8251/v1). Read-only. JSON only.

The reference documentation is generated from the route schemas and is always current:

  • Swagger UI: /v1/docs
  • OpenAPI 3.1: /v1/openapi.json

This page only explains the conventions and gives examples. Do not edit endpoint descriptions here; change the zod schemas in apps/api/src/routes/*.ts (CLAUDE.md §357).

# Conventions

# Envelope

Every /v1 response is wrapped:

json
{
  "data": …,
  "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" }],
  "dataRelease": "CancerIndex 2026-09",
  "generatedAt": "2026-09-08T09:33:19.433Z",
  "total": 865, "limit": 50, "offset": 0, "hasMore": true
}

sources are the distinct upstream sources behind data (license and attribution included so you can comply with redistribution terms). Pagination fields appear on list endpoints; limit ≤ 200.

# Identifiers

Entities are addressed by their public id (CI-CAN-00000364) or a human reference: cancer slug (lung-adenocarcinoma), HGNC symbol (BRAF, aliases accepted), variant slug, drug slug, NCT id, PMID. Merged cancers redirect to the surviving entity.

# Errors

json
{ "error": { "code": "not_found", "message": "cancer \"foo\" not found" }, "requestId": "…" }

Codes: bad_request (400, with zod details), unauthorized (401), not_found (404), rate_limited (429), service_unavailable (503), internal (500). Every response carries x-request-id; quote it when reporting a problem.

# Rate limits and API keys

Anonymous: 60 requests/minute per IP. With Authorization: Bearer <key>: the key's own limit (api_keys.rate_limit_per_minute). Headers: x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset, retry-after on 429. Mint a key:

bash
pnpm --filter @cancerindex/api create-key -- --label "Lab X" --email lab@example.org --tier research --rpm 600

Only the sha256 hash and a prefix are stored; the key is printed once.

# Scientific labels

Payload objects that carry a number also carry the category (observed_data, curated_evidence, regulatory_status, computed_metric) and a provenance object (source, dataset, version, retrievedAt, URL). Missing data is null or an empty list — never 0 substituted for "unknown". counters: null means counters have not been computed yet.

# Endpoints (summary)

Method + path Purpose
GET /healthz { ok, dataRelease, db }
GET /v1/cancers list; q, `level=top
GET /v1/cancers/:id entity + aliases + codes + hierarchy (parents/children per hierarchy type, breadcrumbs) + anatomy + counters + completeness + current rankings + last 20 changes
GET /v1/cancers/:id/statistics epidemiology observations with per-row provenance + charting series; filters metric, geography, sex
GET /v1/cancers/:id/survival survival observations with provenance
GET /v1/cancers/:id/genes cohort frequencies (denominators) + curated evidence per gene, cancer + descendants
GET /v1/cancers/:id/variants variants with evidence counts by level/direction/type
GET /v1/cancers/:id/drugs evidence by drug (level/direction/significance) + jurisdiction-aware approvals
GET /v1/cancers/:id/trials trials for the cancer and descendants; status, phase, interventionalOnly
GET /v1/cancers/:id/publications publication edges + literature count windows (query stored)
GET /v1/genes · GET /v1/genes/:symbol genes; detail with cancers (curated + cohort), variants, drugs, literature
GET /v1/variants/:id variant, ClinVar interpretations, CIViC evidence grouped by cancer, edges
GET /v1/drugs · GET /v1/drugs/:id drugs; detail with evidence by cancer, approvals, trials
GET /v1/trials · GET /v1/trials/:nct trial search (q, status, phase, cancer, country); detail with condition/intervention mappings, locations (200), publications
GET /v1/publications/:pmid publication + entity edges (abstract truncated, links)
GET /v1/rankings/metrics metric catalog with available scopes
GET /v1/rankings metric, geography, sex, age, year, level; snapshot metadata + metric definition + rows
GET /v1/rankings/:metric/:cancerId/explain "Why this rank?": inputs, previous rank, neighbours, lineage trace
GET /v1/search?q= cross-entity search, ≤ 20 typed results, exact > alias > prefix > fuzzy
GET /v1/sources · GET /v1/sources/:slug registry, license status, connector health, runs, counts
GET /v1/stats live counts (60 s cache)
GET /v1/changes change events; entityType, entityId, kind, since
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
GET /v1/trials/intelligence · /trials/intelligence/:cancer derived per-cancer trial metrics (`level=top
GET /v1/trials/terminated terminated / withdrawn / suspended studies with registrant-stated reason and keyword category (cancer, reason, status, since)
GET /v1/trials/sites country (or city) aggregates of trial sites (cancer top-level, phase, recruiting, `level=country
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
GET /v1/approvals · /approvals/recent jurisdiction-aware approval records (authority, jurisdiction, cancer, drug, status, from, to, q); recent feed grouped by month
GET /v1/pipeline · /pipeline/summary drug development stage per drug / per drug × top-level cancer (cancer, stage, drug) — formula ci-drug-pipeline-v1
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
GET/POST /v1/admin/* operators only (x-admin-token): connectors, run/pause/resume, runs/:runId, unresolved + resolve, trace, jobs/counters, jobs/rank, audit

# Examples

bash
API=http://127.0.0.1:8251

# health
curl -s $API/healthz

# homepage ticker
curl -s $API/v1/stats | jq .data

# cancers whose name/alias starts with "glio", sorted by active trials
curl -s "$API/v1/cancers?q=glio&sort=active_trials&limit=5" | jq '.data[] | {id, slug, name, counters}'

# one cancer with hierarchy and current rankings
curl -s $API/v1/cancers/lung-adenocarcinoma | jq '.data | {name, primaryNcitCode, hierarchy: .hierarchy.parents, rankings}'

# epidemiology observations with provenance for a top-level cancer, US only
curl -s "$API/v1/cancers/lung-cancer/statistics?geography=USA&sex=all" | jq '.data.series[0]'

# trials recruiting for a cancer and its subtypes
curl -s "$API/v1/cancers/lung-adenocarcinoma/trials?status=RECRUITING&limit=10" | jq '.data[] | {nctId, briefTitle, phases}'

# a gene and its cancers
curl -s $API/v1/genes/BRAF | jq '.data | {symbol, counters, cancers: .cancers.curatedEvidence[0:3]}'

# a variant: evidence separated by cancer
curl -s $API/v1/variants/braf-v600e | jq '.data.evidenceByCancer[] | {cancer: .cancer.name, acceptedItems}'

# ranking: active trials, top-level set
curl -s "$API/v1/rankings?metric=active_trials&level=top&limit=10" | jq '{snapshot: .data.snapshot, top: [.data.rows[] | {rank, name: .cancer.name, value}]}'

# why this rank? (lineage down to raw records)
curl -s "$API/v1/rankings/active_trials/lung-cancer/explain?level=top" | jq '.data | {rank, previousRank, inputs, trace}'

# search
curl -s "$API/v1/search?q=glio" | jq '.data[] | {type, name, match}'

# sources and license status
curl -s $API/v1/sources | jq '.data[] | {slug, licenseStatus, status, health: .connector.health, records: .counts.sourceRecords}'

# authenticated request (higher limit)
curl -s -H "Authorization: Bearer cix_…" $API/v1/stats -D - -o /dev/null | grep -i x-ratelimit

# admin: enqueue a connector run, then watch it
curl -s -X POST -H "x-admin-token: $ADMIN_TOKEN" -H 'content-type: application/json' \
  -d '{"mode":"incremental","maxMinutes":30,"reason":"manual refresh"}' $API/v1/admin/connectors/clinicaltrials/run
curl -s -H "x-admin-token: $ADMIN_TOKEN" $API/v1/admin/connectors | jq '.data[] | {connectorId, health, lastSuccessAt, recentRuns: .recentRuns[0].status}'

# admin: curation queue and resolution
curl -s -H "x-admin-token: $ADMIN_TOKEN" "$API/v1/admin/unresolved?entityKind=cancer&limit=20"
curl -s -X POST -H "x-admin-token: $ADMIN_TOKEN" -H 'content-type: application/json' \
  -d '{"cancerId":"CI-CAN-00000364","reason":"synonym per NCIt"}' $API/v1/admin/unresolved/123/resolve

Resolving an unresolved label adds a curated cancer_aliases row (alias_type = synonym, source_terminology = curation, source_id = the label's source), re-maps trial_conditions rows that carry the same normalised text, writes audit_log and a change_events row; every other connector picks the alias up on its next run through CancerResolver.

# Local development

bash
pnpm dev:api                 # tsx watch, http://127.0.0.1:8251
pnpm --filter @cancerindex/api test      # unit tests + read-only smoke tests (skipped if DB unreachable)
pnpm --filter @cancerindex/api typecheck