The platform receives whole-slide images (WSI) from outside sources such as the GDC/TCGA archive, GTEx and direct uploads. It files each slide under a controlled vocabulary, checks the file, cuts it into patches, extracts features on GPU servers, and trains or runs diagnosis models on those features.
The API has two audiences:
/api/v1/taxonomy…) and use /resolve to check that a label will be
accepted.
All responses are JSON. Send Accept: application/json so that validation errors come
back as JSON instead of a redirect. Timestamps are ISO-8601.
Every endpoint under /api/v1 requires a server API key. Keys belong to server records
(admin → Settings → Servers). Only keys on active servers are accepted. Send the key
in either header:
Authorization: Bearer <api_key>
X-API-Key: <api_key>
Keys are compared in constant time. The server that owns the key is recorded against the calls it makes; for example, a feature-extraction report is stamped with the reporting server.
| Status | Body | Meaning |
|---|---|---|
401 | {"success":false,"message":"Missing Authorization Bearer token."} | No key was sent. |
403 | {"success":false,"message":"Invalid API key."} | The key is unknown, or its server is inactive. |
GET /api/health is the only endpoint that works without a key. Use it to test connectivity.
Every slide is filed under a three-level tree. The third level can nest further:
| Level | Table | What it is | Identity rule |
|---|---|---|---|
| Organ | organs |
The anatomical site; the root of the tree. Organs are curated by an administrator and only ever selected elsewhere, never typed in, so a disease cannot end up under the wrong site. | Name is unique. |
| Category | categories |
A clinical group inside one organ, for example Tumor or Normal. Because the group belongs to its organ, "Tumor" of the breast and "Tumor" of the lung are two separate groups. | UNIQUE(organ_id, label_en) |
| Disease | disease_subtypes |
A disease entity inside a category. A disease can hold finer diseases through
parent_id; for example, Malignant can be refined into
IDC and ILC. |
UNIQUE(organ_id, name) at every depth: within one organ, a disease name
identifies exactly one clinical entity. |
"Malignant" is refined further — pick one of: IDC, ILC.organ_id from its category and never sets it on its own. If a category is moved
to another organ, its diseases move with it. The move is refused if the category already has
slides, or if one of its disease names already exists in the destination organ.samples.disease_subtype text on every slide filed under it./resolve accepts an inactive row and adds a
warning.
Each row has a qualified name that reads as a path, for example
Breast › Tumor › Malignant › IDC. The /resolve endpoint returns this path.
This is the current tree for active organs, generated from the database when the page loads.
GET /api/v1/taxonomy returns the same data as JSON. The number after each name is its
id, which the API accepts in place of the name.
These organs exist and can be chosen, but no clinical groups are defined under them yet, so a slide cannot be filed against them until an administrator adds one:
Adrenal gland #18 Bladder #11 Bowel #7 Cervix #15 Eye #23 Head & Neck #16 Kidney #3 Liver #5 Lymph #17 Ovary #13 Pancreas #8 Pleura #22 Skin #9 Soft tissue #21 Stomach #6 Testis #20 Thymus #19 Thyroid #10 Uterus #14
The stain says how the tissue was prepared. It is recorded next to the tree, not inside it. A stain
can be matched by id, by abbreviation or by full name, ignoring case. For immunohistochemistry
stains, the marker column names the target protein.
| ID | Abbreviation | Name | Type | Marker |
|---|---|---|---|---|
| 1 | H&E |
Hematoxylin & Eosin | Routine | — |
| 2 | MT |
Masson's Trichrome | Special | — |
| 3 | PAS |
Periodic Acid-Schiff | Special | — |
| 4 | PAS-D |
Periodic Acid-Schiff with Diastase | Special | — |
| 5 | AB |
Alcian Blue | Special | — |
| 6 | CR |
Congo Red | Special | — |
| 7 | Reticulin |
Silver (Reticulin) Stain | Special | — |
| 8 | GMS |
Gomori Methenamine Silver | Special | — |
| 9 | ZN |
Ziehl-Neelsen (AFB) | Special | — |
| 10 | ORO |
Oil Red O | Special | — |
| 11 | Movat |
Movat Pentachrome | Special | — |
| 12 | IHC-ER |
Immunohistochemistry — ER | Immunohistochemistry (IHC) | ER |
| 13 | IHC-PR |
Immunohistochemistry — PR | Immunohistochemistry (IHC) | PR |
| 14 | IHC-HER2 |
Immunohistochemistry — HER2 | Immunohistochemistry (IHC) | HER2 |
| 15 | IHC-Ki67 |
Immunohistochemistry — Ki67 | Immunohistochemistry (IHC) | Ki67 |
| 16 | IHC-p53 |
Immunohistochemistry — p53 | Immunohistochemistry (IHC) | p53 |
| 17 | IHC-PDL1 |
Immunohistochemistry — PD-L1 | Immunohistochemistry (IHC) | PD-L1 |
| 18 | IHC-CD3 |
Immunohistochemistry — CD3 | Immunohistochemistry (IHC) | CD3 |
| 19 | IHC-CD20 |
Immunohistochemistry — CD20 | Immunohistochemistry (IHC) | CD20 |
| 20 | IHC-TTF1 |
Immunohistochemistry — TTF-1 | Immunohistochemistry (IHC) | TTF-1 |
| 21 | IHC-EGFR |
Immunohistochemistry — EGFR | Immunohistochemistry (IHC) | EGFR |
| 22 | FISH |
Fluorescence In-Situ Hybridisation | In-Situ Hybridisation (ISH) | — |
| 23 | CISH |
Chromogenic In-Situ Hybridisation | In-Situ Hybridisation (ISH) | — |
| 24 | Pap |
Papanicolaou | Cytology | — |
| 25 | Giemsa |
Giemsa | Cytology | — |
| 26 | DQ |
Diff-Quik | Cytology | — |
Allowed types: routine, special, IHC, ISH,
fluorescent, cytology, other. Names and abbreviations are
unique.
How a slide's stain is set. A stain chosen at upload is kept. Otherwise:
routine stain, so it should be
reviewed afterwards.
These settings decide how a slide is processed. They also decide which models can read its features:
a model only accepts features made with the same patch size, magnification and feature extractor it
was trained on. GET /api/v1/taxonomy/reference returns all three tables below.
| ID | Label | Value | Folder name | Active |
|---|---|---|---|---|
| 1 | x10 | 10× | 10x | yes |
| 2 | x20 | 20× | 20x | yes |
| 3 | x40 | 40× | 40x | yes |
| ID | Patch | Overlap | WSI level | Active |
|---|---|---|---|---|
| 1 | 224 × 224 px | 0 px | 0 | yes |
| 2 | 256 × 256 px | 0 px | 0 | yes |
| 4 | 256 × 256 px | 64 px | 0 | yes |
| 3 | 512 × 512 px | 0 px | 0 | yes |
| ID | Name | Full name |
|---|---|---|
| 3 | BRACS | Istituto Nazionale Tumori IRCCS - Fondazione Pascale |
| 2 | GTEx | Genotype-Tissue Expression |
| 1 | TCGA-BRCA | The Cancer Genome Atlas - Breast Invasive Carcinoma |
| 4 | TCGA-GBM | The Cancer Genome Atlas - Glioblastoma Multiforme |
| 5 | TCGA-LGG | The Cancer Genome Atlas - Brain Lower Grade Glioma |
| 6 | TCGA-LUAD | The Cancer Genome Atlas - Lung Adenocarcinoma |
| 7 | TCGA-LUSC | The Cancer Genome Atlas - Lung Squamous Cell Carcinoma |
| 8 | TCGA-PRAD | The Cancer Genome Atlas - Prostate Adenocarcinoma |
Slides enter the platform in several ways. They do not all carry the same information, so the table below shows which part of the classification each one sets. Every path is idempotent: each slide is identified by a natural key, so running an import again updates rows instead of duplicating them.
| Entry point | Identified by | Sets organ / category / disease? | Notes |
|---|---|---|---|
| GDC manifest TSV admin → Imports |
file_id (GDC UUID) |
No | Creates or updates slide rows with file name, md5, size, state and data source. The barcode is read from the file name. |
GDC metadata.cart.*.json |
file_id; case by GDC case UUID |
No | Adds entity ids, format and access level, and links each slide to its case. |
GDC clinical.cart.*.json, clinical CSV, cohort JSON |
Case UUID; slides matched by file name, then file_id |
No | Writes the patient's clinical record. Only links to slides that already exist; it never creates slides. |
php artisan tcga:import-slides |
file_id |
Yes, all of it | Organ, category, disease and data source are named on the command line
(--organ=Breast --category=tumor --subtype=IDC --source=TCGA-BRCA). The command
refuses to run if any of them does not exist. It downloads with gdc-client, checks size and
md5, uploads to Drive, then writes the row. The stain is the first stain row, H&E. |
php artisan gtex:import-manifest |
GTEx donor id | Case organ only | The tissue "Breast - Mammary Tissue" becomes organ Breast on the
case, taken from the text before the first dash. Writes demographics and the Hardy death
scale. Only donors with a matching slide are imported. |
| Manual upload admin → Samples |
file_name / Drive id / GDC folder UUID |
Yes, chosen in the form | Organ is required. The category must belong to that organ. The disease must belong to that organ and pass the leaf rule. |
| AI workflow intake admin → AI workflow |
varies | Fixed to Breast › tumor, TCGA-BRCA | Built for the IDC vs ILC model, so the organ, category and source are fixed. |
Before sending a slide in from an outside system, call
GET /api/v1/taxonomy/resolve with the organ, category, disease and stain you plan to
use. If it returns 200, the label will be accepted. If it returns 422,
the errors array says exactly which part is wrong and what the valid choices are.
A slide is linked to its case by the patient barcode. The platform tries
entity_submitter_id first, then the file name.
TCGA-AC-A23C-01Z-00-DX1 → TCGA-AC-A23C).GTEX-1117F-0126 → GTEX-1117F).Linking works in both directions: a case that arrives after its slides picks up those orphan slides. Changing a slide's case, organ, category or disease queues a new verification.
A slide imported with only a coarse label, such as "Malignant", often has its exact diagnosis in
the patient's GDC clinical record. php artisan taxonomy:refile-from-clinical moves such
slides down to the matching leaf disease. It is a dry run unless --apply is passed. It
matches on the ICD-O-3 morphology code, not the free-text diagnosis, because the code is a
controlled vocabulary:
| ICD-O-3 | Meaning | Filed as |
|---|---|---|
8500/3 | Infiltrating duct carcinoma, NOS | IDC |
8520/3 | Infiltrating lobular carcinoma, NOS | ILC |
A slide is only moved when all of these hold:
01–09), not adjacent normal
tissue (10–19). The diagnosis belongs to the patient, not to every
slide the patient has;
Mixed tumours (8522/3, 8523/3, 8524/3) are distinct entities
and need a pathologist's decision, so they are reported and left unchanged.
Every slide is checked before it can be used. The checks are: can OpenSlide open the file, is the
file intact, and can a region be read. The platform also checks that the slide has at least 2
pyramid levels, is at least 1024 px on each side, was scanned at 20× or more, and has a valid
microns-per-pixel value. It needs enough tissue (at least 10 % of the area or 50 patches) and a
usable label. The clinical fields patient id, case id, project, gender and age at index must be
present. A slide is failed if any check fails, needs_clinical_info if only
clinical fields are missing, and otherwise passed or pending. Blur,
artefact and background scores only produce warnings.
| Stage | Column | Values | Driven by |
|---|---|---|---|
| Storage | storage_status |
not_downloaded · downloading · verifying · available · corrupted · missing · upload_failed |
Upload to Google Drive |
| Verification | slide_verifications.verification_status |
pending · passed · failed · needs_clinical_info |
Verification job |
| Quality | quality_status |
pending · passed · rejected · needs_review · needs_clinical_info |
Mirrors verification |
| Patches | tiling_status |
pending · processing · done · failed |
Patch extraction on the platform host |
| Features | feature_extraction_status |
pending · processing · completed · failed |
GPU server → POST /api/v1/feature-extraction/report |
| Training run | training_runs.status |
pending · processing · completed · failed · cancelled |
CLAM server → /api/v1/training/* |
| Inference run | inference_runs.status |
pending · processing · completed · failed |
CLAM server → /api/v1/inference/* |
A slide can be used for training only when its features are completed, its quality is
passed, its verification is passed, and it is marked usable.
Drive paths follow the tree, so files are laid out by source, organ and group:
samples/{source}/{organ}/{category}/{file_id}/{file_name}
sliced_slides/{magnification}/{source}/{organ}/{category}/{case}/sample_{id}_{px}px/patches.tar.gz
features/{model}/{magnification}/{source}/{organ}/{category}/{case}/sample_{id}_{px}px/
/api/healthno authChecks connectivity. Needs no key.
curl https://ai.histopathology.cloud/api/health
{"success":true,"service":"histopathology-management-api","time":"2026-09-24T10:00:00+00:00"}
/api/v1/healthAPI keyChecks that your key works, and returns the name of the server the key belongs to.
curl -H "Authorization: Bearer $KEY" https://ai.histopathology.cloud/api/v1/health
{"success":true,"server":"runpod-titan-1","time":"2026-09-24T10:00:00+00:00"}
/api/v1/taxonomyAPI keyReturns the full tree: organ → categories → diseases, nested to any depth.
| Query | Type | Description |
|---|---|---|
organ | name or id | Return only this organ. Unknown organ → 404. |
include_inactive | boolean | Include inactive organs, categories and diseases. |
curl -H "Authorization: Bearer $KEY" "https://ai.histopathology.cloud/api/v1/taxonomy?organ=Breast"
{
"success": true,
"levels": ["organ", "category", "disease"],
"max_disease_depth": 5,
"organs": [{
"id": 1, "name": "Breast", "is_active": true,
"categories": [{
"id": 4, "label": "Tumor", "is_active": true,
"diseases": [{
"id": 7, "name": "Malignant", "depth": 1, "is_leaf": false, "is_active": true,
"children": [
{"id": 11, "name": "IDC", "depth": 2, "is_leaf": true, "is_active": true, "children": []},
{"id": 12, "name": "ILC", "depth": 2, "is_leaf": true, "is_active": true, "children": []}
]
}]
}]
}],
"unrooted_categories": []
}
is_leaf counts inactive children too, which is how the leaf rule counts them. Only
diseases with is_leaf: true can be used to label a slide.
unrooted_categories lists groups with no organ; they cannot be used.
/api/v1/taxonomy/organsAPI keyReturns a flat list of organs, with the number of categories and diseases under each.
Accepts include_inactive.
{"success":true,"organs":[{"id":1,"name":"Breast","is_active":true,"categories_count":2,"diseases_count":3}, …]}
/api/v1/taxonomy/stainsAPI keyReturns the stain vocabulary (section 5). Accepts include_inactive.
{"success":true,"stains":[{"id":1,"name":"Hematoxylin & Eosin","abbreviation":"H&E","type":"routine",
"type_label":"Routine","marker":null,"description":"…","is_active":true}, …]}
/api/v1/taxonomy/referenceAPI keyReturns magnifications, patch sizes and data sources (section 6).
{"success":true,
"magnifications":[{"id":2,"label":"x20","value":20,"folder_name":"20x","is_active":true}, …],
"patch_sizes":[{"id":1,"size_px":224,"overlap_px":0,"wsi_level":0,"label":"224×224 px","ai_model_id":1,"is_active":true}, …],
"data_sources":[{"id":1,"name":"TCGA-BRCA","full_name":"…","base_url":null,"is_active":true}, …]}
/api/v1/taxonomy/resolveAPI keyChecks a label and turns names into ids. It applies the same rules the platform uses when it files a slide, and it writes nothing. Each parameter accepts a name (ignoring case) or an id.
| Query | Required | Matched against |
|---|---|---|
organ | yes | organ name |
category | yes | category label, only within that organ |
disease | no | disease name within that organ. It must sit in the given category and be a leaf. |
stain | no | abbreviation, then full name |
curl -G -H "Authorization: Bearer $KEY" https://ai.histopathology.cloud/api/v1/taxonomy/resolve \
--data-urlencode organ=Breast --data-urlencode category=Tumor \
--data-urlencode disease=IDC --data-urlencode "stain=H&E"
HTTP 200
{
"success": true,
"valid": true,
"classification": {
"organ": {"id": 1, "name": "Breast"},
"category": {"id": 4, "label": "Tumor"},
"disease": {"id": 11, "name": "IDC", "parent_id": 7, "depth": 2, "is_leaf": true, "path": ["Malignant", "IDC"]},
"stain": {"id": 1, "name": "Hematoxylin & Eosin", "abbreviation": "H&E"}
},
"qualified_name": "Breast › Tumor › Malignant › IDC",
"errors": [],
"warnings": []
}
An invalid label returns 422 with valid: false. Whatever did resolve is
still returned, and every problem is listed in errors:
GET /api/v1/taxonomy/resolve?organ=Breast&category=Tumor&disease=Malignant
HTTP 422
{"success":true,"valid":false, …,
"errors":["\"Malignant\" is refined further — pick one of: IDC, ILC."],"warnings":[]}
| Error | Cause |
|---|---|
organ is required. / category is required. | The parameter is missing. |
No organ matches "…". | The organ is unknown. |
Organ "X" has no category "…" — choose one of: … | The category exists only in another organ, or not at all. |
Organ "X" has no disease "…". | The disease name is unknown in that organ. |
Disease "D" belongs to category "A", not "B". | The disease is in the right organ but a different group. |
"D" is refined further — pick one of: … | Leaf rule: choose a finer disease. |
No stain matches "…". | The stain is unknown. |
Warnings do not make a label invalid. They report inactive rows, and the case where no disease was given although the category defines diseases, so the slide would be filed at group level only.
/api/v1/feature-extraction/reportAPI keyThe GPU worker reports progress on the job it was given (section 10). Send one call each time the status changes.
| Field | Type | Notes |
|---|---|---|
sample_id | integer, required | Must exist. |
status | required | processing · completed · failed |
slide_id | string | File name, for logs. |
features_gdrive_path, features_gdrive_folder_id, runpod_output_path | string | Where the features were written. Stored on completed. |
patch_count, failed_patch_count | integer ≥ 0 | Stored on completed. |
model_name, model_version | string | The version is checked later against model requirements, for example TITAN. |
error_message | string ≤ 2000 | Stored on failed. |
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -H "Accept: application/json" \
https://ai.histopathology.cloud/api/v1/feature-extraction/report -d '{
"sample_id": 42, "slide_id": "TCGA-AC-A23C-01Z-00-DX1.svs", "status": "completed",
"features_gdrive_path": "features/TITAN/20x/TCGA-BRCA/Breast/Tumor/…/sample_42_224px",
"patch_count": 5312, "failed_patch_count": 0, "model_name": "TITAN", "model_version": "TITAN-v1"
}'
{"success":true,"message":"Status updated.","sample":{"id":42,"status":"completed"}}
/api/v1/feature-extraction/jobs/{sample}API keyReturns the platform's current record of a slide's feature extraction. A worker can use it to resume after a restart.
{"success":true,"sample":{"id":42,"slide_id":"TCGA-….svs","feature_extraction_status":"completed",
"feature_extraction_completed_at":"…","features_gdrive_path":"…","features_gdrive_folder_id":null,
"features_runpod_path":null,"features_patch_count":5312,"features_failed_patch_count":0,
"features_model_version":"TITAN-v1","feature_extraction_error":null}}
/api/v1/servers/{serverId}/update-urlAPI keyA worker registers its own public URL when it boots. Body: {"api_url":"https://…"}.
The URL is always written to the server that owns the key; {serverId} in the path
is ignored, so one server can never overwrite another's URL.
/api/v1/training/progressAPI keySent once per epoch. Each call is appended to metrics.history, and the run is marked
processing.
{"run_id": 17, "epoch": 3, "total_epochs": 50,
"metrics": {"train_loss": 0.41, "val_loss": 0.47, "val_auc": 0.91}}
/api/v1/training/reportAPI keyThe final result. status is one of completed · failed ·
cancelled. If metrics is omitted, the existing metrics are kept.
{"run_id": 17, "status": "completed", "metrics": {…},
"model_path": "training/CLAM/run_17/best_model.pt", "error": null}
Both training endpoints return {"ok":true}, or 404 {"error":"Run not found"}.
/api/v1/inference/progressAPI keyOptional progress messages. The first one moves the run from pending to
processing.
{"inference_run_id": 5, "stage": "loading_features", "message": "Loading .h5 features from Drive"}
/api/v1/inference/reportAPI keyThe final result. status is completed or failed.
class_label is a label from the training run's label_map.
{"inference_run_id": 5, "status": "completed",
"prediction": {"class_label": "IDC", "class_index": 0, "confidence": 0.923,
"probabilities": {"0": 0.923, "1": 0.077}},
"attention_map_path": "inference/results/run_5/attention.png", "error": null}
Both inference endpoints return {"ok":true}, or 404 {"error":"Inference run not found"}.
A GPU worker must serve these endpoints. The platform calls them with the worker's own key as the
Bearer token. Every worker must answer GET /health without authentication. The platform
calls /health before each dispatch: if it is unhealthy, the job is retried after 60 s;
if it is unreachable, after 90 s.
| Call | Purpose |
|---|---|
POST {worker}/jobs/start | Start one slide. The worker replies {"status":"accepted","job_id":"…"}, and the platform keeps the job_id. |
GET {worker}/jobs/{job_id} | Used to resume a stalled operation. queued or running means the job is still alive; any other answer re-queues it. |
POST {worker}/jobs/start
{
"sample_id": 42,
"slide_id": "TCGA-AC-A23C-01Z-00-DX1.svs",
"patch_size_px": 224,
"magnification": "x20", "magnification_folder": "20x",
"gdrive_input_path": "sliced_slides/20x/TCGA-BRCA/Breast/Tumor/…/sample_42_224px",
"gdrive_input_archive": "patches.tar.gz",
"gdrive_output_path": "features/TITAN/20x/TCGA-BRCA/Breast/Tumor/…/sample_42_224px",
"ai_model": {"id": 1, "name": "TITAN", "slug": "…", "huggingface": "…", "version": "v1",
"embedding_dim": "768", "input_resolution": "512x512"},
"callback": {"url": "https://ai.histopathology.cloud/api/v1/feature-extraction/report", "token": "<api_key>", "method": "POST"},
"dispatched_at": "2026-09-24T10:00:00+00:00"
}
A 401 or 403 from the worker fails the job at once. A 404 or
5xx is retried.
This server's /health must report "service":"clam_training". This stops a
training run from being sent to a feature-extraction worker.
| Call | Body (main fields) |
|---|---|
POST {server}/training/start |
run_id, feature_model, samples_train / samples_val / samples_test
(each [{sample_id, label, training_phase, gdrive_features_path, parent_label?}]),
training_params (model_type, epochs, learning_rate, bag_size, n_classes,
use_class_weights, seed, dropout, …, plus n_parent_classes, hier_weight,
child_to_parent for hierarchical runs), label_map, parent_label_map, gdrive_output_dir |
POST {server}/inference/start |
inference_run_id, model_checkpoint_path, feature_model, slide_features_path,
slide_name, n_classes, model_type, label_map, gdrive_output_dir |
A training run turns the tree into class labels, in one of three ways:
label_type | One class per | Hierarchy |
|---|---|---|
category | category (clinical group) | flat |
disease_subtype | disease | Two levels: each disease (fine class) is paired with its category (coarse class). If there is more than one category, the model also learns the coarse level. |
disease_type | the case's GDC disease_type text | flat |
A run is limited to one organ: the organ is the scope, the category is the coarse class, and the disease is the fine class. Labels are fixed when the run is dispatched, so editing a slide afterwards cannot change what the model was trained on. Other rules:
| Model | Classes | Requires |
|---|---|---|
idc_ilc_titan_v1IDC vs ILC — TITAN features, linear |
IDC ILC |
feature model: TITANpatch px: 224magnification: 20xmin patches: 100 |
Before a slide is scored, its features are checked against the model's requirements. If they were
made with a different feature extractor, patch size or magnification, or have too few patches, the
slide is refused and the reason is given. A model's classes are plain names, for example
IDC; they are not linked to disease ids in the tree.
| Status | Meaning |
|---|---|
200 | OK. |
401 / 403 | The key is missing, or it is invalid or inactive (section 2). |
404 | The sample, run or organ is unknown. |
422 | Validation failed. Laravel returns {"message":"…","errors":{"field":["…"]}}. For /resolve it means the label cannot be filed (see above). |
5xx | Server error. Retry with backoff. Callbacks are idempotent, so resending the same final report is safe. |
Accept: application/json.403 with a key that used to work: the server may have been marked
inactive, or its key was rotated in admin → Settings → Servers.needs_clinical_info: import the case's
clinical record (GDC clinical JSON or CSV) so that gender and age at index are present.