cBioPortal logoAPI

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.

VerbRouteRole
discoveryGET graph/integrations/vendorsany authenticated caller
importPOST graph/integrations/cbioportal/import/{projectId}Admin
auditPOST graph/integrations/cbioportal/audit/{projectId}Viewer
audit (API key)POST v1/{projectId}/integrations/cbioportal/auditViewer
re-auditPOST graph/integrations/cbioportal/reaudit/{projectId}Viewer
historyGET graph/integrations/cbioportal/history/{projectId}Viewer
badgeGET graph/integrations/cbioportal/badge/{projectId}Viewer
badge (API key)GET v1/{projectId}/integrations/cbioportal/badgeViewer
generatePOST graph/integrations/cbioportal/generate/{projectId}Viewer
statusGET 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 fieldValue for our study
Vendorcbioportal
Project Nameexample_brca_2026
Imported At2026-08-03T09:14:22.7431180+00:00
Fingerprintc4a71f2e9b380d56
CountsfieldsAdded=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 Atabsent

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.