Every cBioPortal route, every role: the CoreModels HTTP surface for study governance
Ask the API what it can do for cBioPortal before you write a line of client code:
Every cBioPortal route, every role: the CoreModels HTTP surface for study governance
Ask the API what it can do for cBioPortal before you write a line of client code:
curl -sS "https://coremodels.example.com/graph/integrations/vendors" \
-H "Authorization: Bearer $TOKEN" | jq '.vendors[] | select(.key == "cbioportal")'
{
"key": "cbioportal",
"displayName": "cBioPortal",
"capabilities": "Import, Audit, Generate",
"artifacts": {
"clinical_patient": "data_clinical_patient.txt — at least one clinical file is required (data rows may be omitted)",
"clinical_sample": "data_clinical_sample.txt — at least one clinical file is required (data rows may be omitted)",
"meta": "optional — meta_study.txt (key: value lines; study id/name/cancer type)"
}
}
That capabilities string is the contract for this article. cBioPortal declares Import, Audit, and
Generate, so all three verbs below are real operations rather than stubs. It does not declare
LiveSync, which is why nothing in this API ever asks you for portal credentials.
Two surfaces carry the routes. The interactive surface under graph/integrations/... takes your
normal CoreModels login token and carries every verb. The machine-to-machine surface under v1/...
accepts revocable user API keys and carries the two verbs automation needs: audit and badge. Both
are [Authorize]d, and every project-scoped route enforces a role.
| Verb | Route | Role |
|---|---|---|
| discovery | GET graph/integrations/vendors | any authenticated caller |
| import | POST graph/integrations/cbioportal/import/{projectId} | Admin |
| audit | POST graph/integrations/cbioportal/audit/{projectId} | Viewer |
| audit (API key) | POST v1/{projectId}/integrations/cbioportal/audit | Viewer |
| re-audit | POST graph/integrations/cbioportal/reaudit/{projectId} | Viewer |
| history | GET graph/integrations/cbioportal/history/{projectId} | Viewer |
| badge | GET graph/integrations/cbioportal/badge/{projectId} | Viewer |
| badge (API key) | GET v1/{projectId}/integrations/cbioportal/badge | Viewer |
| generate | POST graph/integrations/cbioportal/generate/{projectId} | Viewer |
| status | GET graph/integrations/cbioportal/status/{projectId} | Viewer |
Below, $PROJECT_ID is the 32-character hex id of the governing CoreModels project and $TOKEN is
a bearer token — a login token on the interactive surface, a user API key on v1.
Import (Admin)
Import is the only cBioPortal verb that writes model shape, and it writes additively: existing governed nodes are never mutated or deleted.
POST /graph/integrations/cbioportal/import/$PROJECT_ID
Authorization: Bearer $TOKEN
Content-Type: application/json
{
"artifacts": {
"clinical_patient": "#Patient Identifier\tAge\n#Unique patient identifier\tAge at diagnosis\n#STRING\tNUMBER\n#1\t1\nPATIENT_ID\tAGE\n",
"clinical_sample": "…",
"meta": "type_of_cancer: brca\ncancer_study_identifier: example_brca_2026\nname: Example BRCA Cohort 2026\n"
},
"spaces": []
}
The body is the shared ArtifactsRequest: artifacts maps artifact name to raw content and is
required; spaces is an optional array of space ids, where empty means the project's main space.
Artifact keys are matched case-insensitively, and an entry whose value is blank is dropped before
parsing — so sending "meta": "" is the same as not sending meta at all.
The response reports counts plus the two honesty channels, lossiness and errors:
{
"success": true,
"vendor": "cbioportal",
"projectName": "example_brca_2026",
"datasetsAdded": 2,
"datasetsSkippedExisting": 0,
"fieldsAdded": 0,
"lineageEdgesAdded": 0,
"lineageEdgesSkipped": 0,
"nodesEnriched": 10,
"snapshotStored": true,
"lossiness": [],
"errors": []
}
projectName comes from cancer_study_identifier in the meta file; without a meta file it falls
back to cbioportal-study. lineageEdgesAdded is always 0 here — clinical staging files describe
no build dependencies, so the patient/sample join is written as a governed reference rather than a
lineage edge. fieldsAdded counts only attributes added to instruments that were already governed,
so a first import reports zero while a later import that introduces a column reports one per column.
Two failure shapes are worth handling explicitly. An unknown vendor segment returns
{"success": false, "errors": [{"path": "vendor", "message": "Unknown vendor 'cbio'. Registered: …"}]}
with the full registered list, and a missing artifact map returns the same envelope with
Body must include 'artifacts': { "<name>": "<content>" } (e.g. manifest for dbt). Parse failures
are more specific: a file without four # header rows fails with
Not a clinical staging file (needs 4 '#' header rows above the attribute-ID row). and sending
neither clinical file fails with
At least one clinical staging file ('clinical_patient' or 'clinical_sample') is required.
Audit (Viewer)
Audit compares fresh artifacts against the governed graph and writes nothing — unless you pass
recordHistory, which is opt-in bookkeeping and defaults to false.
POST /graph/integrations/cbioportal/audit/$PROJECT_ID
Authorization: Bearer $TOKEN
Content-Type: application/json
{
"artifacts": { "clinical_patient": "…", "clinical_sample": "…" },
"recordHistory": true
}
The report shape is stable across every audit route:
{
"success": true,
"vendor": "cbioportal",
"projectName": "example_brca_2026",
"errorCount": 1,
"warningCount": 1,
"infoCount": 1,
"codes": {
"field-type-drift": 1,
"attribute-id-not-upper": 1,
"attribute-no-description": 1
},
"driftedObjects": ["patient.AGE"],
"fingerprint": "c4a71f2e9b380d56",
"metrics": {
"Datasets (estate)": "2",
"Datasets governed": "2 / 2",
"Fields governed": "8 / 8",
"Governed nodes with canonical mappings": "0 / 10 (0%)",
"Last import": "2026-08-03T09:14:22.7431180+00:00"
},
"findings": [
{
"section": "Drift",
"severity": "Error",
"code": "field-type-drift",
"subject": "patient.AGE",
"message": "Field type changed since the last import.",
"detail": "governed: NUMBER, estate: STRING"
},
{
"section": "Conformance",
"severity": "Info",
"code": "attribute-no-description",
"subject": "patient",
"message": "1 clinical attribute(s) carry no description row — curators downstream will guess.",
"detail": "OS_STATUS"
},
{
"section": "Conformance",
"severity": "Warning",
"code": "attribute-id-not-upper",
"subject": "sample.cancerType",
"message": "Attribute IDs must be UPPER_CASE for cBioPortal validation to pass.",
"detail": null
}
],
"markdown": "# cbioportal Schema Audit — example_brca_2026\n\n🔴 **1 errors · 1 warnings · 1 info**\n…",
"historyRecorded": true,
"lossiness": []
}
Parse errorCount for gating, codes for policy (a map of finding code to occurrence count),
driftedObjects for "what exactly moved", and fingerprint — a 16-character content hash of the
audited artifacts — to tell one submission from the next. markdown renders the whole report with
the verdict line first and each section folded into a collapsible block.
The v1 twin takes the identical body and wraps the same payload in the standard API envelope, so
every field moves one level down:
curl -sS -X POST \
"https://coremodels.example.com/v1/$PROJECT_ID/integrations/cbioportal/audit" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
--data-binary @audit-request.json | jq '.data.errorCount'
Runs recorded from v1 carry the trigger ci; runs recorded from the interactive route carry
audit.
Re-audit (Viewer)
Audit asks whether fresh files still conform to the governed model. Re-audit asks the mirror question: does the governed model still match the last-known study? It runs the same engine over the snapshot stored at import time against the current governed view.
POST /graph/integrations/cbioportal/reaudit/$PROJECT_ID
Authorization: Bearer $TOKEN
Content-Type: application/json
{ "projectName": null, "spaces": null }
projectName: null re-audits the most recently stored snapshot; naming a study targets that one.
The response is the same report shape with an extra metric, Snapshot stored, and
historyRecorded: true — unlike audit, re-audit always records its run. With no stored snapshot
you get a precise refusal: No stored estate snapshot for vendor 'cbioportal' — import the vendor project first (imports persist the parsed snapshot).
History (Viewer)
GET /graph/integrations/cbioportal/history/$PROJECT_ID
Authorization: Bearer $TOKEN
{
"success": true,
"vendor": "cbioportal",
"projects": [
{
"projectName": "example_brca_2026",
"runs": [
{
"at": "2026-08-03T11:02:47.1180930+00:00",
"trigger": "ci",
"errorCount": 1,
"warningCount": 1,
"infoCount": 1,
"codes": { "field-type-drift": 1, "attribute-id-not-upper": 1, "attribute-no-description": 1 },
"fingerprint": "c4a71f2e9b380d56"
}
]
}
]
}
Runs are newest first, one trail per vendor-side study, capped at the 50 most recent runs. Triggers
are audit, ci, reaudit, and scheduled. Records are deliberately compact — counts, codes, and
fingerprint, never full findings — because the trail answers "is this study drifting over time?"
while a live audit answers "what exactly is wrong now?"
Badge (Viewer)
GET /graph/integrations/cbioportal/badge/$PROJECT_ID
GET /v1/$PROJECT_ID/integrations/cbioportal/badge
Both return image/svg+xml: a self-contained shields-style SVG labeled cbioportal audit, green
when the latest recorded run was clean, yellow when it had warnings only, red with the error count
when it had errors, and gray when nothing has been recorded yet. Both routes are authenticated like
everything else, so send the Authorization header rather than expecting an anonymous image URL.
Generate (Viewer)
Generate closes the loop back to staging files. It reads the governed model and emits one
upload-ready data_clinical_{instrument}.txt scaffold per governed Type.
POST /graph/integrations/cbioportal/generate/$PROJECT_ID
Authorization: Bearer $TOKEN
Content-Type: application/json
{ "typeNames": ["patient", "sample"] }
The GenerateRequest body also accepts targetVersion, an open extra string map, and spaces;
the cBioPortal connector reads none of those dialect options, so typeNames is the one that
matters. It matters more than it looks: generation walks every governed Type in the project, not
only the ones cBioPortal imported. In a project that also governs other estates, scope it.
{
"success": true,
"artifacts": [
{
"name": "data_clinical_patient.txt",
"kind": "tsv",
"content": "#PATIENT_ID\tAGE\tSEX\tOS_STATUS\n#Unique patient identifier\tAge at diagnosis in years\tSex at birth\tOS_STATUS\n#STRING\tNUMBER\tSTRING\tSTRING\n#1\t1\t1\t1\nPATIENT_ID\tAGE\tSEX\tOS_STATUS\n"
}
],
"lossiness": [],
"errors": []
}
Decoded, that artifact is a valid five-line header block:
#PATIENT_ID AGE SEX OS_STATUS
#Unique patient identifier Age at diagnosis in years Sex at birth OS_STATUS
#STRING NUMBER STRING STRING
#1 1 1 1
PATIENT_ID AGE SEX OS_STATUS
Read the rows honestly. The display-name row is the governed Element label, not the original
cbio.displayName the import captured — so a round trip through CoreModels writes attribute IDs
into row one unless someone relabels the Elements with human-readable names. The description row
falls back to the label when no governed description exists (OS_STATUS above). Datatypes derive
from the governed value types: integer and floating-point become NUMBER, boolean becomes
BOOLEAN, everything else becomes STRING. Priorities are emitted as 1 across the board, and
attribute IDs are minted upper case from the label, so a governed label of Age at dx becomes
AGE_AT_DX. A governed Type with no elements is skipped with a declared StructuralDrop in
lossiness; when nothing at all is eligible, the call fails with No eligible governed types found to emit as staging-file scaffolds.
Status (Viewer)
GET /graph/integrations/cbioportal/status/$PROJECT_ID
Authorization: Bearer $TOKEN
The response is { "success": true, "vendor": "cbioportal", "imported": true, "state": { … }, "governedDatasets": 2 }. imported is simply whether a state record exists, and governedDatasets
counts the vendor identities in this project that resolve to a governed Type — 2 for a study with
both instruments.
The nested state record is what the last import wrote to the integration state node:
| State field | Value for our study |
|---|---|
| Vendor | cbioportal |
| Project Name | example_brca_2026 |
| Imported At | 2026-08-03T09:14:22.7431180+00:00 |
| Fingerprint | c4a71f2e9b380d56 |
| Counts | fieldsAdded=0, lineageAdded=0, lineageSkipped=0, nodesEnriched=10 |
| Facts | {"instruments":"2","attributes":"8","cancerType":"brca","studyId":"example_brca_2026","studyName":"Example BRCA Cohort 2026"} |
| Tool Version / Artifact Version / Generated At | absent |
The three absent fields are absent on purpose: clinical staging files declare no tool version and no
generation stamp, and the connector does not invent one. Facts is the parser's own key-value
record, carrying the instrument and attribute counts alongside the three keys read from the meta
file.
What this API deliberately does not offer
There is no live-portal route, because there is no credential store to make one from. There is also
no cBioPortal-specific reconciliation: the project-wide POST graph/integrations/reconcile/{projectId}
route (Admin) matches datasets two vendors govern by their physical relation name, and clinical
instruments have no database.schema.table coordinates to match on. And reaudit plus history
live on the interactive surface only — the API-key surface carries audit and badge.
For the artifact contract and a copy-paste extraction recipe, see the cBioPortal quickstart in the CoreModels integration docs.