Skill Tracer
Skills › Scientific Agent Skills › imaging-data-commons › rest_api_guide.md

rest_api_guide.md

644 lines · skills/imaging-data-commons/references/rest_api_guide.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# IDC REST API Guide
2
3**Verified 2026-09-30:** all 21 REST routes, OpenAPI request/response contracts, and representative validation errors. API `3.0.0b3` (build `0640860`), IDC data version v24, `idc_index_data_version` 24.2.2
4
5IDC operates a hosted REST API that exposes discovery, cohort building, metadata SQL, and
6download manifests over plain HTTP. No authentication, account, or credentials are required —
7HTTP examples use `curl`; the download step separately requires `idc-index` or a transfer tool.
8
9The API and the [MCP server](mcp_guide.md) are the same service behind two transports: the MCP
10tools wrap these endpoints. Both are hosted against a current IDC release, independently of the
11`idc-index` version installed locally.

IDC REST API Guide

Verified 2026-09-30: all 21 REST routes, OpenAPI request/response contracts, and representative validation errors. API 3.0.0b3 (build 0640860), IDC data version v24, idc_index_data_version 24.2.2

IDC operates a hosted REST API that exposes discovery, cohort building, metadata SQL, and download manifests over plain HTTP. No authentication, account, or credentials are required — HTTP examples use curl; the download step separately requires idc-index or a transfer tool.

The API and the MCP server are the same service behind two transports: the MCP tools wrap these endpoints. Both are hosted against a current IDC release, independently of the idc-index version installed locally.

Raw lines 1 to 12
Uses an outside tool MCP :9
13## When to Use This Guide
15Load this guide when you need to:
16- Query IDC from a language or environment with no `idc-index` install (shell, R, Java, JS, a
17 notebook without pip access)
18- Build cohorts and manifests over HTTP for a pipeline or web application
19- Run metadata SQL against a current IDC release without downloading local index files
20- Hand a user copy-pasteable `curl` commands they can run anywhere
22For downloads, pandas/notebook analysis, reading pixel data, DICOMweb, BigQuery, or digital
23pathology tiling, use `idc-index` as documented in `SKILL.md`. The API never moves image bytes:
24it returns public `s3://` URLs and manifests, and the transfer happens directly from cloud
25storage to the client.
27**Choosing among the three interfaces:**
29| Situation | Use |
30|-----------|-----|
31| Session already has the hosted MCP server | MCP tools (`mcp_guide.md`) — same data, no HTTP plumbing |
32| `idc-index` already installed, results feed pandas / downloads | `idc-index` (`SKILL.md`) |
33| `idc-index` **not** installed and the task is read-only metadata | This REST API — do not install to answer a metadata question |
34| No Python, or a non-Python client, or shell commands the user re-runs | This REST API |
35| Installed `idc-index` is a whole IDC data release behind and cannot be upgraded here | REST API for the query, **direct bucket transfer** may be needed for the download — `idc-index` cannot fetch what its index does not list |
37Being hosted, the API always serves a current IDC release — but so does an up-to-date
38`idc-index`. "I want the newest data" is not on its own a reason to prefer the API: the normal
39fix for a stale local index is to upgrade it. Reach for the API on version grounds only when
40upgrading is not an option in that environment. Avoiding the install *is* a reason, though: the
41packaged index data is ~77 MB before pandas, pyarrow, and duckdb, which a metadata question does
42not need.

When to Use This Guide

Load this guide when you need to:

  • Query IDC from a language or environment with no idc-index install (shell, R, Java, JS, a notebook without pip access)
  • Build cohorts and manifests over HTTP for a pipeline or web application
  • Run metadata SQL against a current IDC release without downloading local index files
  • Hand a user copy-pasteable curl commands they can run anywhere

For downloads, pandas/notebook analysis, reading pixel data, DICOMweb, BigQuery, or digital pathology tiling, use idc-index as documented in SKILL.md. The API never moves image bytes: it returns public s3:// URLs and manifests, and the transfer happens directly from cloud storage to the client.

Choosing among the three interfaces:

SituationUse
Session already has the hosted MCP serverMCP tools (mcp_guide.md) — same data, no HTTP plumbing
idc-index already installed, results feed pandas / downloadsidc-index (SKILL.md)
idc-index not installed and the task is read-only metadataThis REST API — do not install to answer a metadata question
No Python, or a non-Python client, or shell commands the user re-runsThis REST API
Installed idc-index is a whole IDC data release behind and cannot be upgraded hereREST API for the query, direct bucket transfer may be needed for the download — idc-index cannot fetch what its index does not list

Being hosted, the API always serves a current IDC release — but so does an up-to-date idc-index. "I want the newest data" is not on its own a reason to prefer the API: the normal fix for a stale local index is to upgrade it. Reach for the API on version grounds only when upgrading is not an option in that environment. Avoiding the install is a reason, though: the packaged index data is ~77 MB before pandas, pyarrow, and duckdb, which a metadata question does not need.

Raw lines 13 to 43
Uses an outside tool MCP :31
44## Endpoint and Versioning
46| Property | Value |
47|----------|-------|
48| Base URL | `https://api.imaging.datacommons.cancer.gov/v3` |
49| Authentication | None |
50| Content type | `application/json` (request and response), except `cohort/manifest.txt` → `text/plain` |
51| Interactive docs | https://api.imaging.datacommons.cancer.gov/v3/docs (Swagger UI) |
52| OpenAPI spec | https://api.imaging.datacommons.cancer.gov/v3/openapi.json |
53| Source | https://github.com/ImagingDataCommons/IDC-REST-MCP |
55**v3 is in beta.** The contract may still change before the final `3.0.0` release, so verify the
56running build rather than assuming the values in this guide:
58```bash
59curl -s https://api.imaging.datacommons.cancer.gov/v3/version
60# {"idc_version":"v24","idc_index_data_version":"24.2.2","api_version":"3.0.0b3","build":"0640860"}
61```
63`idc_version` is the IDC data release the API serves and is the authority when the API is in
64use — prefer it over the `idc-data-version` pinned in the `SKILL.md` frontmatter, which records
65the release the skill was last verified against.

Endpoint and Versioning

PropertyValue
Base URLhttps://api.imaging.datacommons.cancer.gov/v3
AuthenticationNone
Content typeapplication/json (request and response), except cohort/manifest.txt → text/plain
Interactive docshttps://api.imaging.datacommons.cancer.gov/v3/docs (Swagger UI)
OpenAPI spechttps://api.imaging.datacommons.cancer.gov/v3/openapi.json
Sourcehttps://github.com/ImagingDataCommons/IDC-REST-MCP

v3 is in beta. The contract may still change before the final 3.0.0 release, so verify the running build rather than assuming the values in this guide:

curl -s https://api.imaging.datacommons.cancer.gov/v3/version
# {"idc_version":"v24","idc_index_data_version":"24.2.2","api_version":"3.0.0b3","build":"0640860"}

idc_version is the IDC data release the API serves and is the authority when the API is in use — prefer it over the idc-data-version pinned in the SKILL.md frontmatter, which records the release the skill was last verified against.

Raw lines 44 to 66
Uses an outside tool MCP :53
67### Checking the API against a local idc-index
69The API and `idc-index` are built on the same `idc-index-data` package, and both report its
70version, so consistency is an exact check rather than a guess:
72| Side | Data release (coarse) | `idc-index-data` version (exact) |
73|------|-----------------------|----------------------------------|
74| API | `GET /v3/version` → `idc_version` | `GET /v3/version` → `idc_index_data_version` |
75| Local | `IDCClient().get_idc_version()` | `idc_index_data.__version__` |
77```python
78import idc_index_data
79import requests
81response = requests.get("https://api.imaging.datacommons.cancer.gov/v3/version", timeout=30)
82response.raise_for_status()
83api = response.json()
84api_version, local_version = api["idc_index_data_version"], idc_index_data.__version__
86if api_version.split(".")[0] != local_version.split(".")[0]:
87 print(f"Different IDC data release: API {api_version}, local {local_version} — upgrade")
88elif api_version != local_version:
89 print(f"Same data release, different index build: API {api_version}, local {local_version}")
90 # → Same data release, different index build: API 24.2.2, local 24.2.0
91```
93**The major is the IDC data release; the rest is the index build.** `idc-index-data` `24.x.y`
94serves IDC `v24` — 24.0.0 shipped with the v24 release, and 24.1.0 / 24.2.x are later builds of
95the *same* release. What differs between them is the index itself (added tables and columns,
96corrected metadata). Pin the full index build and compare requested UUIDs; matching
97release labels alone do not establish that two queries or exported manifests are identical.
99So read a mismatch by its position:
101| Difference | Means | Consequence |
102|------------|-------|-------------|
103| Major (24.x.y vs 25.x.y) | Different IDC data release | Series added, revised, or removed. Counts legitimately differ, and `idc-index` **cannot download** what its index does not list |
104| Minor or patch (24.2.0 vs 24.2.2) | Same data release, different index build | Same intended data release; verify selected UUIDs. Metadata corrections can change query results |
106Comparing `idc_version` alone cannot make this distinction in the other direction either — the
107`vNN` label is exactly the major, so matching `v24` on both sides tells you the release agrees
108but says nothing about the index build.
110When the two disagree, say so and name both versions, then reconcile rather than mixing
111results: upgrading `idc-index` brings the local side to the newer `idc-index-data`, and
112`python scripts/check_version.py` reports whether an upgrade is available and prints the
113command for the interpreter you are running. Do not present API-derived and locally-derived counts side by side as if they came
114from one index.
116**A major behind also breaks downloads.** `idc-index` resolves every `s3://` URL it is given
117against its *own* index, so a manifest built from a newer IDC data release can name series it
118has never heard of — see *When the local index is a data release behind the API* under
119**Getting the Data**.

Checking the API against a local idc-index

The API and idc-index are built on the same idc-index-data package, and both report its version, so consistency is an exact check rather than a guess:

SideData release (coarse)idc-index-data version (exact)
APIGET /v3/version → idc_versionGET /v3/version → idc_index_data_version
LocalIDCClient().get_idc_version()idc_index_data.__version__
import idc_index_data
import requests

response = requests.get("https://api.imaging.datacommons.cancer.gov/v3/version", timeout=30)
response.raise_for_status()
api = response.json()
api_version, local_version = api["idc_index_data_version"], idc_index_data.__version__

if api_version.split(".")[0] != local_version.split(".")[0]:
    print(f"Different IDC data release: API {api_version}, local {local_version} — upgrade")
elif api_version != local_version:
    print(f"Same data release, different index build: API {api_version}, local {local_version}")
    # → Same data release, different index build: API 24.2.2, local 24.2.0

The major is the IDC data release; the rest is the index build. idc-index-data 24.x.y serves IDC v24 — 24.0.0 shipped with the v24 release, and 24.1.0 / 24.2.x are later builds of the same release. What differs between them is the index itself (added tables and columns, corrected metadata). Pin the full index build and compare requested UUIDs; matching release labels alone do not establish that two queries or exported manifests are identical.

So read a mismatch by its position:

DifferenceMeansConsequence
Major (24.x.y vs 25.x.y)Different IDC data releaseSeries added, revised, or removed. Counts legitimately differ, and idc-index cannot download what its index does not list
Minor or patch (24.2.0 vs 24.2.2)Same data release, different index buildSame intended data release; verify selected UUIDs. Metadata corrections can change query results

Comparing idc_version alone cannot make this distinction in the other direction either — the vNN label is exactly the major, so matching v24 on both sides tells you the release agrees but says nothing about the index build.

When the two disagree, say so and name both versions, then reconcile rather than mixing results: upgrading idc-index brings the local side to the newer idc-index-data, and python scripts/check_version.py reports whether an upgrade is available and prints the command for the interpreter you are running. Do not present API-derived and locally-derived counts side by side as if they came from one index.

A major behind also breaks downloads. idc-index resolves every s3:// URL it is given against its own index, so a manifest built from a newer IDC data release can name series it has never heard of — see When the local index is a data release behind the API under Getting the Data.

Raw lines 67 to 120
Runs code python scripts/check_version.py :112
121### Use v3 only — V1 and V2 are being retired
123**Do not write new code against the V1 or V2 IDC APIs, and do not follow V1/V2 examples found
124in older tutorials, notebooks, blog posts, or forum answers.** Both are superseded by v3 and
125are scheduled to be deprecated and shut down; code written against them will stop working. If
126a user brings V1 or V2 code, say so and port it to v3 rather than extending it.
128Two signals that a snippet is V1/V2 rather than v3, both of which will fail against `/v3`:
130- a base URL other than `https://api.imaging.datacommons.cancer.gov/v3` (for example a
131 `/v1/` or `/v2/` path segment)
132- per-attribute filter suffixes such as `Modality_btw` or `_lt` / `_gt`, instead of v3's
133 separate `terms` and `ranges` objects
135V1/V2 documentation survives only in the [IDC docs archive](https://learn.canceridc.dev/archive/archive)
136for historical reference. Treat it as read-only history, not as a source of working examples.

Use v3 only — V1 and V2 are being retired

Do not write new code against the V1 or V2 IDC APIs, and do not follow V1/V2 examples found in older tutorials, notebooks, blog posts, or forum answers. Both are superseded by v3 and are scheduled to be deprecated and shut down; code written against them will stop working. If a user brings V1 or V2 code, say so and port it to v3 rather than extending it.

Two signals that a snippet is V1/V2 rather than v3, both of which will fail against /v3:

  • a base URL other than https://api.imaging.datacommons.cancer.gov/v3 (for example a /v1/ or /v2/ path segment)
  • per-attribute filter suffixes such as Modality_btw or _lt / _gt, instead of v3's separate terms and ranges objects

V1/V2 documentation survives only in the IDC docs archive for historical reference. Treat it as read-only history, not as a source of working examples.

Raw lines 121 to 137
138## The Query Surfaces
140The endpoints group into five surfaces that build on each other:
142| Surface | Answers | Endpoints |
143|---------|---------|-----------|
144| Discovery | What exists? What can I filter on? | `GET /version`, `/stats`, `/collections`, `/collections/{id}`, `/analysis_results`, `/attributes`, `/attributes/{attr}/values` |
145| Cohort | How big is my selection, and what's in it? | `POST /cohort/counts`, `POST /cohort/manifest` |
146| Retrieval | Give me the download links | `POST /cohort/manifest.txt` |
147| SQL | Anything relational or aggregate | `GET /tables`, `GET /tables/{table}`, `POST /sql` |
148| Side tools | View / cite / license-check a cohort | `GET /viewer-url`, `POST /citations`, `POST /licenses` |
150Discovery supplies the vocabulary (attribute names and their valid values) that the cohort
151filters consume. SQL is the escape hatch for questions structured filters cannot express.
153Clinical data has its own discovery endpoints (`GET /clinical/tables`,
154`/clinical/tables/{table}`, `/clinical/tables/{table}/rows`) and is filtered or joined through
155SQL against the `clinical` schema.

The Query Surfaces

The endpoints group into five surfaces that build on each other:

SurfaceAnswersEndpoints
DiscoveryWhat exists? What can I filter on?GET /version, /stats, /collections, /collections/{id}, /analysis_results, /attributes, /attributes/{attr}/values
CohortHow big is my selection, and what's in it?POST /cohort/counts, POST /cohort/manifest
RetrievalGive me the download linksPOST /cohort/manifest.txt
SQLAnything relational or aggregateGET /tables, GET /tables/{table}, POST /sql
Side toolsView / cite / license-check a cohortGET /viewer-url, POST /citations, POST /licenses

Discovery supplies the vocabulary (attribute names and their valid values) that the cohort filters consume. SQL is the escape hatch for questions structured filters cannot express.

Clinical data has its own discovery endpoints (GET /clinical/tables, /clinical/tables/{table}, /clinical/tables/{table}/rows) and is filtered or joined through SQL against the clinical schema.

Raw lines 138 to 156
157## Endpoint Reference
159All paths are relative to `https://api.imaging.datacommons.cancer.gov/v3`.
161| Method & path | Purpose | Key response fields |
162|---------------|---------|---------------------|
163| `GET /version` | IDC data release + API build | `idc_version`, `idc_index_data_version`, `api_version`, `build` |
164| `GET /stats` | Headline totals | `collections`, `patients`, `studies`, `series`, `instances`, `size_TB` |
165| `GET /collections` | List collections (JSON array) | `collection_id`, `collection_name`, `cancer_types`, `tumor_locations`, `species`, `subjects`, `description` |
166| `GET /collections/{id}` | Collection detail | the above plus `patients`, `studies`, `series`, `instances`, `size_TB`, `modalities[]`, `licenses[]` |
167| `GET /analysis_results` | Derived datasets (JSON array) | `analysis_result_id`, `analysis_result_title`, `source_DOI`, `subjects`, `collections`, `modalities`, `license_short_name` |
168| `GET /attributes` | Filterable attributes | `name`, `table`, `data_type`, `kind` (`term` \| `range`), `categorical`, `description` |
169| `GET /attributes/{attr}/values?limit=` | Distinct values with counts | `attribute`, `values[{value,count}]`, `truncated`, `note` |
170| `GET /tables` | Tables available to SQL | `tables[{name,description,column_count}]` |
171| `GET /tables/{table}` | Column schema | `name`, `description`, `columns[{name,type,description}]` |
172| `GET /clinical/tables?collection_id=` | Clinical tables, optionally one collection | `tables[{table_name,sql_path,collection_id,column_count}]` |
173| `GET /clinical/tables/{table}` | Clinical columns + labels | `name`, `columns[{name,type,description}]` |
174| `GET /clinical/tables/{table}/rows?max_rows=` | Clinical rows (capped) | `columns`, `rows`, `row_count`, `truncated`, `max_rows` |
175| `POST /cohort/counts` | Distinct counts for a filter (cheap) | `patients`, `studies`, `series`, `instances`, `size_TB` |
176| `POST /cohort/manifest` | Counts + page of series + download payload | `counts`, `page`, `page_size`, `returned`, `total_series`, `series[]`, `download` |
177| `POST /cohort/manifest.txt` | Full manifest as `text/plain` | one `s3://…/*` URL per line |
178| `POST /sql` | Guarded read-only SQL (DuckDB) | `columns`, `rows`, `row_count`, `truncated`, `max_rows` |
179| `GET /viewer-url` | OHIF / Slim viewer link | `viewer_url`, `viewer`, `study_instance_uid`, `series_instance_uid` |
180| `POST /citations` | Citations for a cohort | `format`, `citations[]`, `idc_acknowledgment`, `recommendation` |
181| `POST /licenses` | License breakdown for a cohort | `licenses[{license_short_name,series,size_TB}]` |
183`GET /v3/health` and `GET /v3` (API root) also exist for liveness checks.

Endpoint Reference

All paths are relative to https://api.imaging.datacommons.cancer.gov/v3.

Method & pathPurposeKey response fields
GET /versionIDC data release + API buildidc_version, idc_index_data_version, api_version, build
GET /statsHeadline totalscollections, patients, studies, series, instances, size_TB
GET /collectionsList collections (JSON array)collection_id, collection_name, cancer_types, tumor_locations, species, subjects, description
GET /collections/{id}Collection detailthe above plus patients, studies, series, instances, size_TB, modalities[], licenses[]
GET /analysis_resultsDerived datasets (JSON array)analysis_result_id, analysis_result_title, source_DOI, subjects, collections, modalities, license_short_name
GET /attributesFilterable attributesname, table, data_type, kind (term \range), categorical, description
GET /attributes/{attr}/values?limit=Distinct values with countsattribute, values[{value,count}], truncated, note
GET /tablesTables available to SQLtables[{name,description,column_count}]
GET /tables/{table}Column schemaname, description, columns[{name,type,description}]
GET /clinical/tables?collection_id=Clinical tables, optionally one collectiontables[{table_name,sql_path,collection_id,column_count}]
GET /clinical/tables/{table}Clinical columns + labelsname, columns[{name,type,description}]
GET /clinical/tables/{table}/rows?max_rows=Clinical rows (capped)columns, rows, row_count, truncated, max_rows
POST /cohort/countsDistinct counts for a filter (cheap)patients, studies, series, instances, size_TB
POST /cohort/manifestCounts + page of series + download payloadcounts, page, page_size, returned, total_series, series[], download
POST /cohort/manifest.txtFull manifest as text/plainone s3://…/* URL per line
POST /sqlGuarded read-only SQL (DuckDB)columns, rows, row_count, truncated, max_rows
GET /viewer-urlOHIF / Slim viewer linkviewer_url, viewer, study_instance_uid, series_instance_uid
POST /citationsCitations for a cohortformat, citations[], idc_acknowledgment, recommendation
POST /licensesLicense breakdown for a cohortlicenses[{license_short_name,series,size_TB}]

GET /v3/health and GET /v3 (API root) also exist for liveness checks.

Raw lines 157 to 184
185## Filter Syntax
187Cohort filters are shared by `cohort/counts`, `cohort/manifest`, `cohort/manifest.txt`,
188`licenses`, and `citations`. **The filter object always goes under a `filters` key**, on every one
189of them. It has two parts:
191- **`terms`** — `{attribute: [values]}` for equality/membership. Values are **OR**'d within an
192 attribute and **AND**'d across attributes.
193- **`ranges`** — `{attribute: {"gte": x, "lte": y}}` for numeric and date attributes. Either
194 bound may be omitted for an open-ended range.
196```json
198 "filters": {
199 "terms": {"Modality": ["CT"], "collection_id": ["nlst"]},
200 "ranges": {"instanceCount": {"gte": 100, "lte": 200}}
201 }
203```
205Filters operate only on the `index` table's filterable attributes — 19 of them as of `3.0.0b3`:
207| Kind | Attributes |
208|------|------------|
209| `term` | `collection_id`, `analysis_result_id`, `PatientID`, `StudyInstanceUID`, `SeriesInstanceUID`, `Modality`, `BodyPartExamined`, `Manufacturer`, `ManufacturerModelName`, `PatientSex`, `sop_class_name`, `license_short_name`, `source_DOI` |
210| `range` | `instanceCount`, `series_size_MB`, `series_init_idc_version`, `series_revised_idc_version`, `StudyDate`, `SeriesDate` |
212`SeriesInstanceUID`, `StudyInstanceUID`, and `PatientID` being filterable is what makes the
213side tools work at any granularity — the licenses or citations for a single series are just a
214one-value filter.
216Anything outside this list — clinical values, segmented anatomy, per-modality acquisition
217parameters — is not filterable here; use the SQL surface instead.
219**Ground values before filtering.** Call `GET /attributes` for what is filterable and whether
220it is a term or a range, then `GET /attributes/{attr}/values` for real values and their casing —
221values are matched case-sensitively.

Filter Syntax

Cohort filters are shared by cohort/counts, cohort/manifest, cohort/manifest.txt, licenses, and citations. **The filter object always goes under a filters key**, on every one of them. It has two parts:

  • **terms** — {attribute: [values]} for equality/membership. Values are OR'd within an attribute and AND'd across attributes.
  • **ranges** — {attribute: {"gte": x, "lte": y}} for numeric and date attributes. Either bound may be omitted for an open-ended range.
{
  "filters": {
    "terms": {"Modality": ["CT"], "collection_id": ["nlst"]},
    "ranges": {"instanceCount": {"gte": 100, "lte": 200}}
  }
}

Filters operate only on the index table's filterable attributes — 19 of them as of 3.0.0b3:

KindAttributes
termcollection_id, analysis_result_id, PatientID, StudyInstanceUID, SeriesInstanceUID, Modality, BodyPartExamined, Manufacturer, ManufacturerModelName, PatientSex, sop_class_name, license_short_name, source_DOI
rangeinstanceCount, series_size_MB, series_init_idc_version, series_revised_idc_version, StudyDate, SeriesDate

SeriesInstanceUID, StudyInstanceUID, and PatientID being filterable is what makes the side tools work at any granularity — the licenses or citations for a single series are just a one-value filter.

Anything outside this list — clinical values, segmented anatomy, per-modality acquisition parameters — is not filterable here; use the SQL surface instead.

Ground values before filtering. Call GET /attributes for what is filterable and whether it is a term or a range, then GET /attributes/{attr}/values for real values and their casing — values are matched case-sensitively.

Raw lines 185 to 222
223### The server reports what it filtered on
225Filtered JSON responses echo `filters_applied` and `warnings` (nested under `counts` in a
226manifest response), and misuse is refused rather
227than ignored. Together these make a result self-describing, so you do not have to sanity-check a
228count against a number you happen to remember.
230| Mistake | Response |
231|---------|----------|
232| Bare filter object, no `filters` key | `422` naming the correct shape |
233| Unrecognized key at any depth (`term` for `terms`, a range bound spelled `min`) | `422` pointing at the key |
234| Unknown filter attribute, or a range attribute used as a term | `400` naming the discovery call |
235| Unfiltered `cohort/manifest` or `manifest.txt` | `400` — it will not enumerate the whole archive |
236| Unfiltered `cohort/counts` or `licenses` | `200` plus an explicit "ENTIRE IDC archive" warning |
237| A predicate that constrains nothing (`{"collection_id": []}`, `{"instanceCount": {}}`) | `200`, and `warnings` names the ignored predicate |
238| Miscased value (`mr` for `MR`) | `200`, zero counts, and a warning naming the casing that exists |
240```bash
241curl -s $B/cohort/counts -H 'content-type: application/json' \
242 -d '{"filters": {"terms": {"collection_id": ["rider_pilot"]}}}'
243# {"patients":8,"studies":154,"series":774,"instances":21111,"size_TB":0.011,
244# "filters_applied":{"terms":{"collection_id":["rider_pilot"]},"ranges":{}},"warnings":[]}
245```
247**Read `warnings` before reporting any count.** A zero count with `warnings: []` means the filter
248applied and matched nothing — that is a real answer. A zero count *with* a casing warning means
249the filter was wrong. And `filters_applied` covers the case no shape check can: a request
250carrying one good predicate plus one that constrains nothing returns a perfectly plausible
251number, and only `warnings` reveals the dropped half.
253`cohort/manifest.txt` returns `text/plain` and so cannot carry these fields; there the
254required-predicate `400` does the same job, appending the ignored-predicate reason to its message.

The server reports what it filtered on

Filtered JSON responses echo filters_applied and warnings (nested under counts in a manifest response), and misuse is refused rather than ignored. Together these make a result self-describing, so you do not have to sanity-check a count against a number you happen to remember.

MistakeResponse
Bare filter object, no filters key422 naming the correct shape
Unrecognized key at any depth (term for terms, a range bound spelled min)422 pointing at the key
Unknown filter attribute, or a range attribute used as a term400 naming the discovery call
Unfiltered cohort/manifest or manifest.txt400 — it will not enumerate the whole archive
Unfiltered cohort/counts or licenses200 plus an explicit "ENTIRE IDC archive" warning
A predicate that constrains nothing ({"collection_id": []}, {"instanceCount": {}})200, and warnings names the ignored predicate
Miscased value (mr for MR)200, zero counts, and a warning naming the casing that exists
curl -s $B/cohort/counts -H 'content-type: application/json' \
  -d '{"filters": {"terms": {"collection_id": ["rider_pilot"]}}}'
# {"patients":8,"studies":154,"series":774,"instances":21111,"size_TB":0.011,
#  "filters_applied":{"terms":{"collection_id":["rider_pilot"]},"ranges":{}},"warnings":[]}

**Read warnings before reporting any count.** A zero count with warnings: [] means the filter applied and matched nothing — that is a real answer. A zero count with a casing warning means the filter was wrong. And filters_applied covers the case no shape check can: a request carrying one good predicate plus one that constrains nothing returns a perfectly plausible number, and only warnings reveals the dropped half.

cohort/manifest.txt returns text/plain and so cannot carry these fields; there the required-predicate 400 does the same job, appending the ignored-predicate reason to its message.

Raw lines 223 to 255
256## Worked Examples

Worked Examples

Raw lines 256 to 257
258### Discovery
260```bash
261B=https://api.imaging.datacommons.cancer.gov/v3
263curl -s $B/version # data release + API build
264curl -s $B/stats # headline totals
265curl -s $B/collections # collection array (176 in the reviewed snapshot)
266curl -s $B/collections/rider_pilot # one collection: counts, modalities, licenses
267curl -s $B/analysis_results # derived datasets (segmentations, annotations)
268curl -s $B/attributes # what can be filtered, and how
269curl -s "$B/attributes/Modality/values?limit=10"
270```

Discovery

B=https://api.imaging.datacommons.cancer.gov/v3

curl -s $B/version                      # data release + API build
curl -s $B/stats                        # headline totals
curl -s $B/collections                  # collection array (176 in the reviewed snapshot)
curl -s $B/collections/rider_pilot      # one collection: counts, modalities, licenses
curl -s $B/analysis_results             # derived datasets (segmentations, annotations)
curl -s $B/attributes                   # what can be filtered, and how
curl -s "$B/attributes/Modality/values?limit=10"
Raw lines 258 to 271
272### Cohort building
274Check size first — `counts` is cheap and answers "is this download sane?":
276```bash
277curl -s $B/cohort/counts \
278 -H 'content-type: application/json' \
279 -d '{"filters": {"terms": {"Modality": ["MR"], "BodyPartExamined": ["BREAST"]}}}'
280# {"patients":3718,"studies":5689,"series":47986,"instances":6493262,"size_TB":2.421,
281# "filters_applied":{...},"warnings":[]}
282```
284Then request a page of series plus the download payload — same filter, same `filters` key.
286```bash
287curl -s $B/cohort/manifest \
288 -H 'content-type: application/json' \
289 -d '{"filters": {"terms": {"Modality": ["MR"], "BodyPartExamined": ["BREAST"]}},
290 "page": 0, "page_size": 3}'
291```
293Each `series[]` row carries `collection_id`, `PatientID`, `StudyInstanceUID`,
294`SeriesInstanceUID`, `Modality`, `SeriesDescription`, `instanceCount`, `series_size_MB`,
295`aws_bucket`, `crdc_series_uuid`, and `series_aws_url`. Set `"include_rows": false` to get
296counts and the download payload without the rows.

Cohort building

Check size first — counts is cheap and answers "is this download sane?":

curl -s $B/cohort/counts \
  -H 'content-type: application/json' \
  -d '{"filters": {"terms": {"Modality": ["MR"], "BodyPartExamined": ["BREAST"]}}}'
# {"patients":3718,"studies":5689,"series":47986,"instances":6493262,"size_TB":2.421,
#  "filters_applied":{...},"warnings":[]}

Then request a page of series plus the download payload — same filter, same filters key.

curl -s $B/cohort/manifest \
  -H 'content-type: application/json' \
  -d '{"filters": {"terms": {"Modality": ["MR"], "BodyPartExamined": ["BREAST"]}},
       "page": 0, "page_size": 3}'

Each series[] row carries collection_id, PatientID, StudyInstanceUID, SeriesInstanceUID, Modality, SeriesDescription, instanceCount, series_size_MB, aws_bucket, crdc_series_uuid, and series_aws_url. Set "include_rows": false to get counts and the download payload without the rows.

Raw lines 272 to 297
298### SQL
300```bash
301curl -s $B/sql \
302 -H 'content-type: application/json' \
303 -d '{"sql": "SELECT Modality, count(*) n FROM index GROUP BY 1 ORDER BY n DESC", "max_rows": 20}'
304```

SQL

curl -s $B/sql \
  -H 'content-type: application/json' \
  -d '{"sql": "SELECT Modality, count(*) n FROM index GROUP BY 1 ORDER BY n DESC", "max_rows": 20}'
Raw lines 298 to 305
306### Viewer, licenses, citations
308```bash
309curl -s "$B/viewer-url?study_instance_uid=1.3.6.1.4.1.14519.5.2.1.7695.4164.129908397467389975396031099306"
311curl -s $B/licenses \
312 -H 'content-type: application/json' \
313 -d '{"filters": {"terms": {"collection_id": ["rider_pilot"]}}}'
315curl -s $B/citations \
316 -H 'content-type: application/json' \
317 -d '{"filters": {"terms": {"collection_id": ["rider_pilot"]}}, "citation_format": "bibtex"}'
318```
320`viewer-url` takes `series_instance_uid` or `study_instance_uid` (and an optional `viewer`
321override); it picks OHIF v3 for radiology and Slim for slide microscopy automatically.
323`citation_format` is one of `apa` (default), `bibtex`, `csl-json`, `turtle`. The response
324separates the per-dataset `citations[]` from `idc_acknowledgment`, the IDC paper — include
325both when publishing.

Viewer, licenses, citations

curl -s "$B/viewer-url?study_instance_uid=1.3.6.1.4.1.14519.5.2.1.7695.4164.129908397467389975396031099306"

curl -s $B/licenses \
  -H 'content-type: application/json' \
  -d '{"filters": {"terms": {"collection_id": ["rider_pilot"]}}}'

curl -s $B/citations \
  -H 'content-type: application/json' \
  -d '{"filters": {"terms": {"collection_id": ["rider_pilot"]}}, "citation_format": "bibtex"}'

viewer-url takes series_instance_uid or study_instance_uid (and an optional viewer override); it picks OHIF v3 for radiology and Slim for slide microscopy automatically.

citation_format is one of apa (default), bibtex, csl-json, turtle. The response separates the per-dataset citations[] from idc_acknowledgment, the IDC paper — include both when publishing.

Raw lines 306 to 326
327### Python client
329```python
330import requests
332BASE = "https://api.imaging.datacommons.cancer.gov/v3"
333session = requests.Session()
335def get(path, **params):
336 r = session.get(f"{BASE}{path}", params=params, timeout=60)
337 r.raise_for_status()
338 return r.json()
340def post(path, payload):
341 r = session.post(f"{BASE}{path}", json=payload, timeout=120)
342 r.raise_for_status()
343 return r.json()
345# 1. Confirm which IDC release the API is serving
346print(get("/version")["idc_version"])
348# 2. Ground the filter values before using them
349modalities = {v["value"] for v in get("/attributes/Modality/values", limit=1000)["values"]}
350assert "MR" in modalities
352# 3. Size the cohort, then page through it — the filter always goes under `filters`
353filters = {"terms": {"collection_id": ["rider_pilot"], "Modality": ["CT"]}}
354counts = post("/cohort/counts", {"filters": filters})
355assert not counts["warnings"], counts["warnings"] # nothing was silently dropped
356print(counts)
358uids = []
359series_rows = []
360page = 0
361while True:
362 manifest = post("/cohort/manifest", {"filters": filters, "page": page, "page_size": 100})
363 assert not manifest["counts"]["warnings"], manifest["counts"]["warnings"]
364 assert manifest["total_series"] == counts["series"], "Cohort changed during pagination"
365 batch = [row["SeriesInstanceUID"] for row in manifest["series"]]
366 uids.extend(batch)
367 series_rows.extend(manifest["series"])
368 if len(uids) >= manifest["total_series"]:
369 break
370 if not batch:
371 raise RuntimeError("Manifest pagination ended before the declared total")
372 page += 1
373assert len(set(uids)) == counts["series"]
375# 4. Hand off to idc-index for the download (see "Handing Off to idc-index" below)
376```
378Error responses raise through `raise_for_status()`. HTTP 400 commonly uses
379`error.message`; HTTP 422 uses `detail[]`. Inspect the actual body before retrying.

Python client

import requests

BASE = "https://api.imaging.datacommons.cancer.gov/v3"
session = requests.Session()

def get(path, **params):
    r = session.get(f"{BASE}{path}", params=params, timeout=60)
    r.raise_for_status()
    return r.json()

def post(path, payload):
    r = session.post(f"{BASE}{path}", json=payload, timeout=120)
    r.raise_for_status()
    return r.json()

# 1. Confirm which IDC release the API is serving
print(get("/version")["idc_version"])

# 2. Ground the filter values before using them
modalities = {v["value"] for v in get("/attributes/Modality/values", limit=1000)["values"]}
assert "MR" in modalities

# 3. Size the cohort, then page through it — the filter always goes under `filters`
filters = {"terms": {"collection_id": ["rider_pilot"], "Modality": ["CT"]}}
counts = post("/cohort/counts", {"filters": filters})
assert not counts["warnings"], counts["warnings"]   # nothing was silently dropped
print(counts)

uids = []
series_rows = []
page = 0
while True:
    manifest = post("/cohort/manifest", {"filters": filters, "page": page, "page_size": 100})
    assert not manifest["counts"]["warnings"], manifest["counts"]["warnings"]
    assert manifest["total_series"] == counts["series"], "Cohort changed during pagination"
    batch = [row["SeriesInstanceUID"] for row in manifest["series"]]
    uids.extend(batch)
    series_rows.extend(manifest["series"])
    if len(uids) >= manifest["total_series"]:
        break
    if not batch:
        raise RuntimeError("Manifest pagination ended before the declared total")
    page += 1
assert len(set(uids)) == counts["series"]

# 4. Hand off to idc-index for the download (see "Handing Off to idc-index" below)

Error responses raise through raise_for_status(). HTTP 400 commonly uses error.message; HTTP 422 uses detail[]. Inspect the actual body before retrying.

Raw lines 327 to 380
381## The SQL Surface
383`POST /sql` runs read-only DuckDB SQL against the same tables `idc-index` exposes locally.
384Ground the schema with `GET /tables` and `GET /tables/{table}` — do not guess table or column
385names.
387**Guardrails** (verified against `3.0.0b3`):
389- Only single read-only `SELECT` / `WITH … SELECT` statements are accepted. Anything else is
390 rejected with `{"error": {"code": "invalid_query", "message": "Only read-only SELECT (or WITH ... SELECT) statements are allowed."}}` and HTTP 400.
391- A server row cap and per-query timeout apply. `max_rows` defaults to **5000** and is clamped
392 to **10000**; results carry `truncated: true` when the cap was hit, and echo the `max_rows`
393 actually applied.
394- Invalid SQL returns DuckDB's own error text, including its "Candidate bindings" suggestions —
395 useful for fixing a misspelled column without another schema round-trip.
397**Tables reachable from SQL** are the `index` table plus the specialized indices documented in
398`SKILL.md` (`collections_index`, `analysis_results_index`, `version_metadata_index`,
399`prior_versions_index`, `seg_index`, `ann_index`, `ann_group_index`, `rtstruct_index`,
400`ct_index`, `mr_index`, `pt_index`, `sm_index`, `sm_instance_index`, `contrast_index`,
401`volume_geometry_index`, `clinical_index`). Join them to `index` on `SeriesInstanceUID`, except
402where `SKILL.md` documents a different key (`segmented_SeriesInstanceUID`,
403`referenced_SeriesInstanceUID`, `collection_id`, `analysis_result_id`).
405The SQL patterns in `references/sql_patterns.md` are written for `client.sql_query()` but the
406SQL itself transfers unchanged — send it as the `sql` field.
408**Array columns:** columns typed `STRING[]` (e.g. `SegmentedPropertyType_CodeMeanings`) hold a
409list per row. Match with `list_contains(col, 'value')`, not `=` or `LIKE`:
411```bash
412curl -s $B/sql -H 'content-type: application/json' -d '{
413 "sql": "SELECT i.collection_id, count(DISTINCT i.SeriesInstanceUID) AS slides FROM index i JOIN seg_index seg ON seg.segmented_SeriesInstanceUID = i.SeriesInstanceUID WHERE i.Modality = '\''SM'\'' AND list_contains(seg.SegmentedPropertyType_CodeMeanings, '\''Nucleus'\'') GROUP BY 1 ORDER BY slides DESC",
414 "max_rows": 20}'
415```
417**A SQL result can be a manifest.** Selecting `series_aws_url` gives download URLs directly,
418and unlike `cohort/manifest.txt` you can carry extra columns alongside them — a per-row
419`license_short_name`, for instance, which the plain manifest does not include:
421```sql
422SELECT SeriesInstanceUID, license_short_name, series_aws_url
423FROM index WHERE collection_id = 'rider_pilot'
424```
426For bulk series, `cohort/manifest.txt` has a separate limit. Compare its nonempty URL-line
427count with `cohort/counts.series`; it has no response-level truncation flag.

The SQL Surface

POST /sql runs read-only DuckDB SQL against the same tables idc-index exposes locally. Ground the schema with GET /tables and GET /tables/{table} — do not guess table or column names.

Guardrails (verified against 3.0.0b3):

  • Only single read-only SELECT / WITH … SELECT statements are accepted. Anything else is rejected with {"error": {"code": "invalid_query", "message": "Only read-only SELECT (or WITH ... SELECT) statements are allowed."}} and HTTP 400.
  • A server row cap and per-query timeout apply. max_rows defaults to 5000 and is clamped to 10000; results carry truncated: true when the cap was hit, and echo the max_rows actually applied.
  • Invalid SQL returns DuckDB's own error text, including its "Candidate bindings" suggestions — useful for fixing a misspelled column without another schema round-trip.

Tables reachable from SQL are the index table plus the specialized indices documented in SKILL.md (collections_index, analysis_results_index, version_metadata_index, prior_versions_index, seg_index, ann_index, ann_group_index, rtstruct_index, ct_index, mr_index, pt_index, sm_index, sm_instance_index, contrast_index, volume_geometry_index, clinical_index). Join them to index on SeriesInstanceUID, except where SKILL.md documents a different key (segmented_SeriesInstanceUID, referenced_SeriesInstanceUID, collection_id, analysis_result_id).

The SQL patterns in references/sql_patterns.md are written for client.sql_query() but the SQL itself transfers unchanged — send it as the sql field.

Array columns: columns typed STRING[] (e.g. SegmentedPropertyType_CodeMeanings) hold a list per row. Match with list_contains(col, 'value'), not = or LIKE:

curl -s $B/sql -H 'content-type: application/json' -d '{
  "sql": "SELECT i.collection_id, count(DISTINCT i.SeriesInstanceUID) AS slides FROM index i JOIN seg_index seg ON seg.segmented_SeriesInstanceUID = i.SeriesInstanceUID WHERE i.Modality = '\''SM'\'' AND list_contains(seg.SegmentedPropertyType_CodeMeanings, '\''Nucleus'\'') GROUP BY 1 ORDER BY slides DESC",
  "max_rows": 20}'

A SQL result can be a manifest. Selecting series_aws_url gives download URLs directly, and unlike cohort/manifest.txt you can carry extra columns alongside them — a per-row license_short_name, for instance, which the plain manifest does not include:

SELECT SeriesInstanceUID, license_short_name, series_aws_url
FROM index WHERE collection_id = 'rider_pilot'

For bulk series, cohort/manifest.txt has a separate limit. Compare its nonempty URL-line count with cohort/counts.series; it has no response-level truncation flag.

Raw lines 381 to 428
429### Clinical data
431Clinical data comes in two layers:
433- **`clinical_index`** — a data dictionary, one row per (collection, table, column) with a
434 human-readable `column_label` and coded `values`. It is an ordinary SQL table and joins to
435 `index` on `collection_id`.
436- **Per-collection clinical tables** (e.g. `nlst_canc`) — the actual rows. There are ~150, so
437 they are kept out of `GET /tables` and discovered through the `/clinical/tables` endpoints
438 instead. In SQL they live under a separate schema and are addressed as `clinical.<table>`.
440Clinical tables join to imaging on **`dicom_patient_id = index.PatientID`**, not on a series
441UID. Clinical data is not harmonized across collections — table and column names vary, so
442always discover before querying.
444```bash
445curl -s "$B/clinical/tables?collection_id=nlst" # which tables this collection has
446curl -s $B/clinical/tables/nlst_canc # columns + labels
447curl -s "$B/clinical/tables/nlst_canc/rows?max_rows=100" # rows, capped
449curl -s $B/sql -H 'content-type: application/json' -d '{
450 "sql": "SELECT count(DISTINCT i.PatientID) AS patients FROM index i JOIN clinical.nlst_canc c ON c.dicom_patient_id = i.PatientID WHERE i.collection_id = '\''nlst'\'' AND i.Modality = '\''CT'\'' AND c.clinical_stag = '\''400'\''"}'
451```
453See `references/clinical_data_guide.md` for value mapping and the wider clinical data model.

Clinical data

Clinical data comes in two layers:

  • **clinical_index** — a data dictionary, one row per (collection, table, column) with a human-readable column_label and coded values. It is an ordinary SQL table and joins to index on collection_id.
  • Per-collection clinical tables (e.g. nlst_canc) — the actual rows. There are ~150, so they are kept out of GET /tables and discovered through the /clinical/tables endpoints instead. In SQL they live under a separate schema and are addressed as clinical.<table>.

Clinical tables join to imaging on **dicom_patient_id = index.PatientID**, not on a series UID. Clinical data is not harmonized across collections — table and column names vary, so always discover before querying.

curl -s "$B/clinical/tables?collection_id=nlst"            # which tables this collection has
curl -s $B/clinical/tables/nlst_canc                       # columns + labels
curl -s "$B/clinical/tables/nlst_canc/rows?max_rows=100"   # rows, capped

curl -s $B/sql -H 'content-type: application/json' -d '{
  "sql": "SELECT count(DISTINCT i.PatientID) AS patients FROM index i JOIN clinical.nlst_canc c ON c.dicom_patient_id = i.PatientID WHERE i.collection_id = '\''nlst'\'' AND i.Modality = '\''CT'\'' AND c.clinical_stag = '\''400'\''"}'

See references/clinical_data_guide.md for value mapping and the wider clinical data model.

Raw lines 429 to 454
455## Getting the Data
457The API returns manifests; a client moves the bytes. Every URL points at public AWS S3 or GCS
458buckets and needs no credentials.
460```bash
461# save a manifest after checking the cohort count
462curl --fail -sS $B/cohort/manifest.txt \
463 -H 'content-type: application/json' \
464 -d '{"filters": {"terms": {"collection_id": ["rider_pilot"]}}}' > idc_manifest.txt
466# download it (needs idc-index installed)
467idc download-from-manifest --manifest-file idc_manifest.txt --download-dir ./idc-data
468```
470The reviewed server also emits a positional `download-from-manifest idc_manifest.txt`
471command in `download.idc_commands`; that command is incompatible with idc-index 0.12.5.
472Use the explicit `--manifest-file` form above, or `idc download idc_manifest.txt`.
474For a filter that is a single `collection_id`, the `download` payload of `cohort/manifest`
475emits the simpler `idc download <collection_id> --download-dir ./idc-data` form.

Getting the Data

The API returns manifests; a client moves the bytes. Every URL points at public AWS S3 or GCS buckets and needs no credentials.

# save a manifest after checking the cohort count
curl --fail -sS $B/cohort/manifest.txt \
  -H 'content-type: application/json' \
  -d '{"filters": {"terms": {"collection_id": ["rider_pilot"]}}}' > idc_manifest.txt

# download it (needs idc-index installed)
idc download-from-manifest --manifest-file idc_manifest.txt --download-dir ./idc-data

The reviewed server also emits a positional download-from-manifest idc_manifest.txt command in download.idc_commands; that command is incompatible with idc-index 0.12.5. Use the explicit --manifest-file form above, or idc download idc_manifest.txt.

For a filter that is a single collection_id, the download payload of cohort/manifest emits the simpler idc download <collection_id> --download-dir ./idc-data form.

Raw lines 455 to 476
477### When the local index is a data release behind the API
479This applies when the two `idc-index-data` **majors** differ — the API serving `25.x.y` against
480a local `24.x.y`, say. Builds of the same release usually resolve the same series, but
481verify selected CRDC UUIDs before treating metadata or download manifests as equivalent.
483`idc download-from-manifest` does not simply hand the URLs to a transfer client: it extracts
484each `crdc_series_uuid` from the manifest and joins it against the **local** index (then
485against `prior_versions_index`) to compute sizes and build the output hierarchy. A manifest
486produced by an API serving a newer IDC data release can therefore contain series the local
487index has never heard of.
489Those rows are **not** downloaded, and the command does not fail — it logs, then continues
490with the rest:
492```
493The total of N copy commands are not recognized as referencing any associated series in the
494main index. ... they may correspond to files available in a release of IDC different from v24
495used in this version of idc-index.
496...
497The corresponding files could not be downloaded.
498```
500The result is a partial download that otherwise looks successful. `download_from_selection(seriesInstanceUID=…)`
501filters the local index: missing UIDs in a partially matched list can be omitted, while
502an entirely unmatched list raises `ValueError` in 0.12.5. Check the requested-minus-found
503UID set before transferring.
505**Fix it one of two ways:**
5071. **Upgrade** — upgrade `idc-index` (`python scripts/check_version.py` prints the command),
508 then re-run. This is the right answer
509 whenever it is possible; it restores the hierarchy, size checks, and progress reporting.
5102. **Bypass the index** — transfer directly from the bucket. The manifest URLs are
511 self-contained (`s3://<bucket>/<crdc_series_uuid>/*`), so no index is needed at all:
513```bash
514# one directory per series, named by crdc_series_uuid
515awk -F/ '{print "cp " $0 " ./idc-data/" $4 "/"}' idc_manifest.txt > s5cmd_commands.txt
516s5cmd --no-sign-request run s5cmd_commands.txt
518# for source=gcs manifests, add the GCS endpoint
519s5cmd --no-sign-request --endpoint-url https://storage.googleapis.com run s5cmd_commands.txt
520```
522`s5cmd run` expects one *command* per line, which is why the bare URLs are rewritten as `cp`
523commands; drop the `$4` segment to land every file flat in `./idc-data/`. `aws s3 cp
524--no-sign-request --recursive` works the same way per URL.
526What you give up by bypassing `idc-index` is convenience, not data: files are laid out under
527CRDC UUIDs instead of `%collection_id/%PatientID/%Modality`, and there is no local size or
528disk-space check — so read `size_TB` from `cohort/counts` first. See
529`references/cloud_storage_guide.md` for bucket layout, `aws`/`gsutil` equivalents, and
530UUID-to-DICOM-UID mapping.
532**`source`: `aws` (default) vs `gcs`.** Both return `s3://` URLs — GCS is reached through its
533S3-compatible endpoint, never a `gs://` URL. That is why `idc download-from-manifest` only
534recognizes `s3://` lines. Driving `s5cmd` yourself, use `--no-sign-request`, and for
535`source=gcs` add `--endpoint-url https://storage.googleapis.com`.
537Report `series` and `size_TB` from `cohort/counts` before a broad download and keep the
538transfer within the user-authorized scope (the reviewed v24 API reports ~99 TB archive-wide).

When the local index is a data release behind the API

This applies when the two idc-index-data majors differ — the API serving 25.x.y against a local 24.x.y, say. Builds of the same release usually resolve the same series, but verify selected CRDC UUIDs before treating metadata or download manifests as equivalent.

idc download-from-manifest does not simply hand the URLs to a transfer client: it extracts each crdc_series_uuid from the manifest and joins it against the local index (then against prior_versions_index) to compute sizes and build the output hierarchy. A manifest produced by an API serving a newer IDC data release can therefore contain series the local index has never heard of.

Those rows are not downloaded, and the command does not fail — it logs, then continues with the rest:

The total of N copy commands are not recognized as referencing any associated series in the
main index. ... they may correspond to files available in a release of IDC different from v24
used in this version of idc-index.
...
The corresponding files could not be downloaded.

The result is a partial download that otherwise looks successful. download_from_selection(seriesInstanceUID=…) filters the local index: missing UIDs in a partially matched list can be omitted, while an entirely unmatched list raises ValueError in 0.12.5. Check the requested-minus-found UID set before transferring.

Fix it one of two ways:

  1. Upgrade — upgrade idc-index (python scripts/check_version.py prints the command), then re-run. This is the right answer whenever it is possible; it restores the hierarchy, size checks, and progress reporting.
  2. Bypass the index — transfer directly from the bucket. The manifest URLs are self-contained (s3://<bucket>/<crdc_series_uuid>/*), so no index is needed at all:
# one directory per series, named by crdc_series_uuid
awk -F/ '{print "cp " $0 " ./idc-data/" $4 "/"}' idc_manifest.txt > s5cmd_commands.txt
s5cmd --no-sign-request run s5cmd_commands.txt

# for source=gcs manifests, add the GCS endpoint
s5cmd --no-sign-request --endpoint-url https://storage.googleapis.com run s5cmd_commands.txt

s5cmd run expects one command per line, which is why the bare URLs are rewritten as cp commands; drop the $4 segment to land every file flat in ./idc-data/. aws s3 cp --no-sign-request --recursive works the same way per URL.

What you give up by bypassing idc-index is convenience, not data: files are laid out under CRDC UUIDs instead of %collection_id/%PatientID/%Modality, and there is no local size or disk-space check — so read size_TB from cohort/counts first. See references/cloud_storage_guide.md for bucket layout, aws/gsutil equivalents, and UUID-to-DICOM-UID mapping.

**source: aws (default) vs gcs.** Both return s3:// URLs — GCS is reached through its S3-compatible endpoint, never a gs:// URL. That is why idc download-from-manifest only recognizes s3:// lines. Driving s5cmd yourself, use --no-sign-request, and for source=gcs add --endpoint-url https://storage.googleapis.com.

Report series and size_TB from cohort/counts before a broad download and keep the transfer within the user-authorized scope (the reviewed v24 API reports ~99 TB archive-wide).

Raw lines 477 to 539
Runs code python scripts/check_version.py :507
540## Limits, Defaults, and Errors
542Verified against `3.0.0b3`. POST SQL and manifest page sizes are clamped; GET query
543parameters outside their OpenAPI bounds return HTTP 422. Read the response limits rather
544than assuming the requested size was honored.
546| Endpoint | Parameter | Default | Cap |
547|----------|-----------|---------|-----|
548| `GET /attributes/{attr}/values` | `limit` | 100 | 10000 |
549| `POST /sql` | `max_rows` | 5000 | 10000 |
550| `GET /clinical/tables/{table}/rows` | `max_rows` | 5000 | 100000 |
551| `POST /cohort/manifest` | `page_size` | 100 | 5000 |
552| `POST /cohort/manifest` | `page` | 0 | — |
553| `POST /cohort/manifest` | `include_rows` | `true` | — |
554| `POST /cohort/manifest.txt` | `limit` | 100000 | No enforced ceiling in reviewed source; keep requests bounded |
555| `POST /cohort/manifest.txt` | `source` | `aws` | — |
556| `POST /citations` | `citation_format` | `apa` | — |
558`cohort/manifest.txt` defaults to 100 000 URL lines and silently drops overflow from the
559text response. It has no pagination or `truncated` field. Compare line count against the
560matching cohort count, then explicitly set a bounded `limit` or partition the cohort.
561The JSON manifest paginates `series[]`; its `download.manifest_truncated` concerns the
562default full-manifest limit, not whether the current page is complete.
564The reviewed server source has no application-level per-caller rate limiter. This does
565not guarantee unlimited infrastructure capacity or exclude HTTP 429 from a gateway.
566Source defaults include a 30 s SQL timeout and 4 GB query memory; deployed settings can differ.
567Honor `Retry-After` on 429 and retry transient 503 responses with bounded backoff. For sustained heavy metadata access, query the `idc-index` Parquet files
568(`references/parquet_access_guide.md`) or BigQuery instead of driving this API hard.
570Size-capped responses carry a `truncated` boolean: `false` means the result is complete, `true`
571means raise the limit or aggregate/narrow instead. Explore narrow, then widen.
573Errors come in two shapes. Semantic problems are HTTP 400 with a uniform body:
575```json
576{"error": {"code": "invalid_query", "message": "Unknown or non-term filter attribute: 'NotAnAttribute'. Use list_attributes to see valid attributes."}}
577```
579Request-shape problems are HTTP 422 with FastAPI's `detail[]` array, which names the offending
580key — a bare filter object, an unrecognized key, or a misspelled range bound all land here. Both
581kinds are actionable: an unknown attribute names the discovery call to make, and a bad column name
582carries DuckDB's candidate bindings. Read the message and fix the request rather than retrying it
583unchanged.
585What does **not** produce an error is a filter that is valid but empty or over-broad. Those return
586HTTP 200 with a `warnings` entry saying so — see *The server reports what it filtered on*. Read
587`warnings`; do not infer from the count alone.

Limits, Defaults, and Errors

Verified against 3.0.0b3. POST SQL and manifest page sizes are clamped; GET query parameters outside their OpenAPI bounds return HTTP 422. Read the response limits rather than assuming the requested size was honored.

EndpointParameterDefaultCap
GET /attributes/{attr}/valueslimit10010000
POST /sqlmax_rows500010000
GET /clinical/tables/{table}/rowsmax_rows5000100000
POST /cohort/manifestpage_size1005000
POST /cohort/manifestpage0—
POST /cohort/manifestinclude_rowstrue—
POST /cohort/manifest.txtlimit100000No enforced ceiling in reviewed source; keep requests bounded
POST /cohort/manifest.txtsourceaws—
POST /citationscitation_formatapa—

cohort/manifest.txt defaults to 100 000 URL lines and silently drops overflow from the text response. It has no pagination or truncated field. Compare line count against the matching cohort count, then explicitly set a bounded limit or partition the cohort. The JSON manifest paginates series[]; its download.manifest_truncated concerns the default full-manifest limit, not whether the current page is complete.

The reviewed server source has no application-level per-caller rate limiter. This does not guarantee unlimited infrastructure capacity or exclude HTTP 429 from a gateway. Source defaults include a 30 s SQL timeout and 4 GB query memory; deployed settings can differ. Honor Retry-After on 429 and retry transient 503 responses with bounded backoff. For sustained heavy metadata access, query the idc-index Parquet files (references/parquet_access_guide.md) or BigQuery instead of driving this API hard.

Size-capped responses carry a truncated boolean: false means the result is complete, true means raise the limit or aggregate/narrow instead. Explore narrow, then widen.

Errors come in two shapes. Semantic problems are HTTP 400 with a uniform body:

{"error": {"code": "invalid_query", "message": "Unknown or non-term filter attribute: 'NotAnAttribute'. Use list_attributes to see valid attributes."}}

Request-shape problems are HTTP 422 with FastAPI's detail[] array, which names the offending key — a bare filter object, an unrecognized key, or a misspelled range bound all land here. Both kinds are actionable: an unknown attribute names the discovery call to make, and a bad column name carries DuckDB's candidate bindings. Read the message and fix the request rather than retrying it unchanged.

What does not produce an error is a filter that is valid but empty or over-broad. Those return HTTP 200 with a warnings entry saying so — see The server reports what it filtered on. Read warnings; do not infer from the count alone.

Raw lines 540 to 588
589## Handing Off to idc-index
591The boundary artifact is a list of `SeriesInstanceUID` values (or a saved `manifest.txt`).
593```python
594# UIDs from a cohort/manifest or /sql response
595series_uids = uids # all pages collected above, not just the last manifest page
597from idc_index import IDCClient
598client = IDCClient()
599local = client.index[client.index["SeriesInstanceUID"].isin(series_uids)]
600local_uuids = dict(zip(local["SeriesInstanceUID"], local["crdc_series_uuid"]))
601mismatched = [r["SeriesInstanceUID"] for r in series_rows
602 if local_uuids.get(r["SeriesInstanceUID"]) != r["crdc_series_uuid"]]
603if mismatched:
604 raise ValueError(f"{len(mismatched)} series absent or revised locally; use the pinned URL manifest")
606client.download_from_selection(
607 downloadDir="./data",
608 seriesInstanceUID=series_uids, # a list, not a DataFrame
609 dirTemplate="%collection_id/%PatientID/%Modality",
611```
613Run `python scripts/check_version.py` before the first `idc-index` call in a session, even when
614discovery happened over the API — the two version independently. Compare
615`idc_index_data_version` on both sides first (see *Checking the API against a local
616idc-index*).
618**This UID handoff must resolve to the same CRDC series versions on both sides.**
619`idc-index` can only select series its local index lists, so when the
620API is a release ahead, UIDs and manifest URLs it returned may resolve to nothing locally:
621`download_from_selection` can omit a missing subset (or raise when none match), and
622`download-from-manifest` logs unrecognized entries and skips them. Do not report that as "no data": name both versions, then either
623upgrade `idc-index` or download straight from the bucket, as described in *When the local index
624is a data release behind the API*.

Handing Off to idc-index

The boundary artifact is a list of SeriesInstanceUID values (or a saved manifest.txt).

# UIDs from a cohort/manifest or /sql response
series_uids = uids  # all pages collected above, not just the last manifest page

from idc_index import IDCClient
client = IDCClient()
local = client.index[client.index["SeriesInstanceUID"].isin(series_uids)]
local_uuids = dict(zip(local["SeriesInstanceUID"], local["crdc_series_uuid"]))
mismatched = [r["SeriesInstanceUID"] for r in series_rows
              if local_uuids.get(r["SeriesInstanceUID"]) != r["crdc_series_uuid"]]
if mismatched:
    raise ValueError(f"{len(mismatched)} series absent or revised locally; use the pinned URL manifest")

client.download_from_selection(
    downloadDir="./data",
    seriesInstanceUID=series_uids,        # a list, not a DataFrame
    dirTemplate="%collection_id/%PatientID/%Modality",
)

Run python scripts/check_version.py before the first idc-index call in a session, even when discovery happened over the API — the two version independently. Compare idc_index_data_version on both sides first (see Checking the API against a local idc-index).

This UID handoff must resolve to the same CRDC series versions on both sides. idc-index can only select series its local index lists, so when the API is a release ahead, UIDs and manifest URLs it returned may resolve to nothing locally: download_from_selection can omit a missing subset (or raise when none match), and download-from-manifest logs unrecognized entries and skips them. Do not report that as "no data": name both versions, then either upgrade idc-index or download straight from the bucket, as described in When the local index is a data release behind the API.

Raw lines 589 to 625
Runs code python scripts/check_version.py :613
626## What the API Does Not Cover
628- **Image bytes.** The API returns URLs and manifests only; files transfer from S3/GCS.
629- **Pixel data access and DICOMweb.** Use `references/dicomweb_guide.md`.
630- **Full DICOM metadata, per-segment detail, SR quantitative/qualitative measurements, private
631 DICOM elements.** Use BigQuery or full public metadata Parquet exports — see `references/bigquery_guide.md`.
632- **Writes.** The service is read-only by construction; there are no POST endpoints that mutate
633 state, and the SQL connection rejects anything but `SELECT`.
634- **Local analysis.** DataFrames, plotting, pydicom/SimpleITK, pathology tiling all stay with
635 `idc-index`.

What the API Does Not Cover

  • Image bytes. The API returns URLs and manifests only; files transfer from S3/GCS.
  • Pixel data access and DICOMweb. Use references/dicomweb_guide.md.
  • Full DICOM metadata, per-segment detail, SR quantitative/qualitative measurements, private DICOM elements. Use BigQuery or full public metadata Parquet exports — see references/bigquery_guide.md.
  • Writes. The service is read-only by construction; there are no POST endpoints that mutate state, and the SQL connection rejects anything but SELECT.
  • Local analysis. DataFrames, plotting, pydicom/SimpleITK, pathology tiling all stay with idc-index.
Raw lines 626 to 636
637## Related Documentation
639- IDC REST API docs: https://learn.canceridc.dev/rest-api/api
640- Swagger UI: https://api.imaging.datacommons.cancer.gov/v3/docs
641- API and MCP server source: https://github.com/ImagingDataCommons/IDC-REST-MCP
642- `references/mcp_guide.md` — the same capabilities as agent tools
643- `references/sql_patterns.md` — SQL that transfers unchanged to `POST /sql`
644- `references/cli_guide.md` — `idc download-from-manifest` and the rest of the CLI

Related Documentation

  • IDC REST API docs: https://learn.canceridc.dev/rest-api/api
  • Swagger UI: https://api.imaging.datacommons.cancer.gov/v3/docs
  • API and MCP server source: https://github.com/ImagingDataCommons/IDC-REST-MCP
  • references/mcp_guide.md — the same capabilities as agent tools
  • references/sql_patterns.md — SQL that transfers unchanged to POST /sql
  • references/cli_guide.md — idc download-from-manifest and the rest of the CLI
Raw lines 637 to 644
Uses an outside tool MCP :641