QIDO-RS — Search for Instances
Query based on ID for DICOM Objects (QIDO-RS) enables searching for studies and series by attributes (PS3.18 §10.6).
| Method | Path | Description |
|---|---|---|
| GET | /studies |
Search for studies |
| GET | /studies/{study}/series |
Search for series in a study |
The Accept header must allow application/dicom+json (application/*,
*/*, or no Accept header at all are accepted). Anything else is rejected
with 406 (Not Acceptable).
Authorization
When viewer-JWT verification is enabled for the deployment, every request to
this interface must carry Authorization: Bearer <jwt>, and results are
restricted to the studies named in the token's scope. See
JWT Authorization for the token format, scope
semantics, and status codes.
Searchability: stored is not searchable
Search queries are served from search indexes maintained automatically by a background study-processing stage after instances are stored.
Newly stored studies become searchable once this processing has completed. After studies have been stored, they are asynchronously processed by the kastoria-health platform. Queries for studies that have not yet been processed return an empty array. A client that stores a study and immediately searches for it must expect this and retry after the processing stage has caught up.
Search parameters
Study level
| Parameter | Support | Description |
|---|---|---|
StudyInstanceUID |
Direct lookup | Unique study identifier |
PatientID |
Exact match, case-insensitive | Patient identifier |
StudyDate |
Single date or range | Study date in YYYYMMDD format. Single date (20240101) or range (20240101-20240131, 20240101-, -20240131) |
PatientName |
Leading components, case-insensitive | Patient's name in DICOM PN form. The value names leading components and each matches whole: DOE matches DOE^JOHN, DOE^JOHN matches DOE^JOHN^Q but not DOE^JOHNSON. Matched against the alphabetic component group |
StudyTime |
Single time or range | Study time in DICOM TM format, HH[MM[SS[.FFFFFF]]]. Single time (143000) or range (080000-170000, 080000-, -170000) |
AccessionNumber |
Exact match, case-insensitive | Accession number |
StudyDescription |
Exact match, case-insensitive | Study description |
ModalitiesInStudy |
List; matches any one modality, ignoring case | Modality (e.g. CT, MR). A list separated by \ or , is an IN clause — the study matches when any of its modalities equals any listed value |
limit |
Supported (default 100, maximum 1000) | Maximum number of matches returned in one response |
offset |
Supported (default 0, maximum 1000) | Number of matches skipped before the first returned one. A value above the maximum is rejected with 400, not capped |
Series level
| Parameter | Support | Description |
|---|---|---|
Modality |
List; matches any one, ignoring case | Filter by modality. A list separated by \ or , is an IN clause |
SeriesInstanceUID |
Exact match | Filter by series UID (a UID has no case to ignore) |
SeriesNumber |
Exact match | Filter by series number |
limit |
Supported (default 100, maximum 1000) | Maximum number of matches returned in one response |
offset |
Supported (default 0, maximum 1000) | Number of matches skipped before the first returned one. A value above the maximum is rejected with 400, not capped |
Attribute IDs
Query parameter keys may be encoded as either of the following (PS3.18 §8.3.4.1):
| Value | Example |
|---|---|
{group}{element} |
0020000D |
{dicomKeyword} |
StudyInstanceUID |
Matching semantics
| Search type | Supported attributes | Example |
|---|---|---|
| Range query | StudyDate, StudyTime |
StudyDate=20240101-20240131 — inclusive range. Either bound may be omitted (e.g. StudyDate=20240101- or StudyDate=-20240131) |
| Exact match | StudyInstanceUID, PatientID, AccessionNumber, StudyDescription, ModalitiesInStudy |
PatientID=PATIENT001 |
| Leading components | PatientName |
PatientName=DOE matches DOE^JOHN — the value names leading PN components and each matches whole, so this is not an exact match on the full name |
Notes:
- A zero-length value is universal matching (PS3.4 §C.2.2.2.1):
PatientName=places no constraint and every study matches. It is neither an error nor an ignored parameter, so it carries noWarningheader field. - Matching ignores case for every attribute except
StudyInstanceUID, which is a UID:aa-1234andAA-1234are one identifier, andchrisandCHRISare one person. StudyTimevalues are compared by the instant they denote, so a query and a stored value written to different precision (090000and090000.000000) match.- The upper bound of a
StudyTimerange runs to the end of the unit it names, matching the way aStudyDateupper bound runs to the end of its day.-235959includes a study acquired at235959.500, and-0900includes one at090030. An explicit fraction names an instant instead, so-090030.000ends exactly there. - A
StudyTimenaming the whole day —000000-235959,000000-,-235959, or a bare-— narrows nothing and is treated as absent rather than searched for. - Each query parameter must be supplied at most once. A repeated parameter —
PatientID=A&PatientID=B— is rejected with400 (Bad Request)naming the parameter. - Wildcard matching is not supported. Wildcard characters (
*,?) are not treated as wildcards in any search parameter. Fuzzy matching is not supported either.
Pagination
Both search transactions implement response pagination per PS3.18 §8.3.4.4.
| Parameter | Default | Behavior |
|---|---|---|
offset |
0 | Number of matches skipped before the first returned match; an explicit value above 1000 is rejected with 400 |
limit |
100 | Maximum matches returned in one response; an explicit value above 1000 is capped |
maxResults (PS3.18 §8.3.4.4.1) is 1000 — the ceiling on an explicit
limit, so no single response returns more than 1000 matches. A request that
supplies no limit receives 100.
When matches remain beyond those returned, the response carries a Warning
header field:
Warning: 299 {service}: There are more additional results that can be requested
The header announces that a further page can be requested. It does not carry a count of the matches not returned.
Both parameters must be non-negative integers. A malformed value is rejected
with 400 (Bad Request) rather than ignored — silently dropping a limit
would return the full result set to a caller that asked for one page.
offset may not exceed 1000 — the same ceiling as limit, so a request
may skip no more than it may retrieve — and a larger one is rejected with
400 (Bad Request) rather than capped. Paging is offset-based with no cursor,
so reaching a match means loading every match in front of it. A caller needing
to go deeper should narrow the query rather than page further into an
unnarrowed one.
Search depth
A search that combines two or more matching attributes is limited in how far back it reaches, but only where it has to be. Naming anything selective — a patient identifier, an accession number — settles the search almost at once, and it is then answered in full however old the matches are. The limit is reached only when every attribute named is a broad one.
Where a search is limited, the response is 200 (OK) and carries a Warning
header field naming the earliest study date covered:
Warning: 299 {service}: Searched studies from 20241216 onward; earlier matches may exist
Results from that date onward are complete. Nothing is claimed about studies before it.
- A
PatientNamesearch names no date. Where such a search is limited, the warning states the limit without dating it:
Warning: 299 {service}: The search was limited before every matching study was examined; further matches may exist
Both spellings mean the same thing to a caller: the result set is not known to be complete. Only the first tells you from when it is.
-
A search on a single matching attribute is not limited at all, and neither is a search on none. Both are answered in full.
-
An empty result set carries the warning too, when the search was limited.
200 (OK)with an empty array and noWarningheader means no study matches.200 (OK)with an empty array and this warning means no study matches within the range searched. These are different answers, and they are distinguished by the header rather than by the payload. -
An explicit
StudyDatenarrows the search but does not remove the limit. It is the most effective thing a caller can supply: it bounds every attribute at once, so a search that names a date is normally answered in full and carries no warning whatever attributes accompany it. -
A caller needing a longer history for a broad combination of attributes should page through it by date, a range at a time.
Unsupported query parameters
A query parameter this service does not match on is accepted, ignored, and
announced. The response is 200 (OK) and carries a Warning header field
naming every parameter that was ignored:
Warning: 299 {service}: The following query parameters are not supported and were ignored: PatientSex\StudyID
This applies to both search transactions, and to any parameter other than that
level's matching attributes and the limit / offset controls — including an
attribute the service returns but cannot match on, and a parameter it does not
recognize at all. The result set is therefore wider than the request asked
for: the ignored parameter narrowed nothing.
A 400 is not used here. PS3.18 reserves it for a query the service cannot
parse, and PS3.4 §C.2.2.1.3 permits an Optional Key to be returned without
being matched on — so a well-formed query the service can only partly honor is
answered, with the shortfall stated.
Parameter names are separated by \, the DICOM multi-valued delimiter, rather
than by a comma, because Warning is itself a comma-delimited list field. A
name this service cannot repeat back is reported as (unprintable):
Warning: 299 {service}: The following query parameters are not supported and were ignored: PatientSex\(unprintable)
Both warnings are sent when both apply — a query naming an ignored attribute can still overflow its page — as two warn-values, the query's shortcoming before the response's:
Warning: 299 {service}: The following query parameters are not supported and were ignored: PatientSex
Warning: 299 {service}: There are more additional results that can be requested
Response
The response is an array of DICOM datasets in DICOM JSON format. Results are
ordered by study date and time, most recent first. One exception: a search on
PatientName that names fewer components than the stored name — SMITH
matching SMITH^JOHN — is ordered most-recent-first within each matching
patient, but is not ordered by date across several patients sharing those
leading components.
Default study-level attributes:
| Tag | Attribute |
|---|---|
| (0020,000D) | StudyInstanceUID |
| (0010,0020) | PatientID |
| (0010,0010) | PatientName |
| (0010,0030) | PatientBirthDate |
| (0010,0040) | PatientSex |
| (0020,0010) | StudyID |
| (0008,0020) | StudyDate |
| (0008,0030) | StudyTime |
| (0008,0050) | AccessionNumber |
| (0008,0061) | ModalitiesInStudy |
| (0008,1030) | StudyDescription |
| (0020,1206) | NumberOfStudyRelatedSeries |
| (0020,1208) | NumberOfStudyRelatedInstances |
Default series-level attributes:
| Tag | Attribute |
|---|---|
| (0020,000D) | StudyInstanceUID |
| (0020,000E) | SeriesInstanceUID |
| (0020,0011) | SeriesNumber |
| (0008,0005) | SpecificCharacterSet |
| (0008,0060) | Modality |
| (0008,0021) | SeriesDate |
| (0008,0031) | SeriesTime |
| (0008,103E) | SeriesDescription |
| (0008,0070) | Manufacturer |
| (0008,0080) | InstitutionName |
| (0008,0090) | ReferringPhysicianName |
| (0008,1090) | ManufacturerModelName |
| (0018,0015) | BodyPartExamined |
| (0018,1030) | ProtocolName |
| (0020,1209) | NumberOfSeriesRelatedInstances |
Note: The includefield parameter, to return additional attributes beyond
the defaults, is not currently supported.
Status codes
| Code | Description |
|---|---|
200 (OK) |
The response payload contains all matching resources (an empty array when there are no matches) |
400 (Bad Request) |
The query cannot be honored as written: a range that cannot match (a StudyDate range whose end precedes its start), a limit or offset that is not a non-negative integer, an offset above 1000, or a query parameter supplied more than once |
401* (Unauthorized) |
Viewer-JWT verification is enabled and the request carries no valid bearer token |
403* (Forbidden) |
A request for a study by UID, or for anything beneath it (series, instances, frames, metadata, etc.), names a Study Instance UID that is not in the token's scope |
406 (Not Acceptable) |
The specified Accept header does not allow application/dicom+json |
500 (Internal Server Error) |
The server encountered an unexpected error |
* See above for an explanation of how to authorize
using Viewer-JWT verification.
Response guarantees
- An empty result set is returned with
200 (OK)and an empty JSON array —204 (No Content)is not used. - A
200 (OK)always means the search ran to completion. A search that fails part-way through returns500, and never a partial or empty array with200— a client must be able to distinguish "no studies match" from "the search could not be completed". A query the service cannot satisfy because of how it is written returns400, so a caller can tell its own mistake from the server's. - A
200 (OK)does not by itself mean every parameter was applied. Two things narrow what the payload holds relative to what was asked, and both are stated in aWarningheader field rather than left for the client to infer: - A parameter this service does not match on is ignored, so the result set is wider than the request.
- Matches beyond
limitare not returned, so the result set is narrower than the whole match set.
A response carrying no Warning header field is complete in every sense:
every parameter was applied, no match was left behind, and no limit was
placed on how far back the search reached.
Examples
Search studies by patient name and date
curl "http://localhost:3000/studies?PatientName=DOE^JOHN&StudyDate=20240101" \
[-H 'Authorization: Bearer <jwt-token>' \ ]
-H "Accept: application/dicom+json"
Search studies by date range
curl "http://localhost:3000/studies?StudyDate=20240101-20240131" \
[-H 'Authorization: Bearer <jwt-token>' \ ]
-H "Accept: application/dicom+json"
Search studies by patient ID
curl "http://localhost:3000/studies?PatientID=PATIENT001" \
[-H 'Authorization: Bearer <jwt-token>' \ ]
-H "Accept: application/dicom+json"
Direct lookup by study UID
curl "http://localhost:3000/studies?StudyInstanceUID=1.2.840.113619.2.55.3.604688119.868.1234567890.1" \
[-H 'Authorization: Bearer <jwt-token>' \ ]
-H "Accept: application/dicom+json"
Paginated search
Skip the first 100 matches and return up to 100 more:
curl "http://localhost:3000/studies?PatientID=PATIENT001&offset=100&limit=100" \
[-H 'Authorization: Bearer <jwt-token>' \ ]
-H "Accept: application/dicom+json"
Search series within a study
curl "http://localhost:3000/studies/1.2.840.113619.2.55.3.604688119.868.1234567890.1/series?Modality=CT" \
[-H 'Authorization: Bearer <jwt-token>' \ ]
-H "Accept: application/dicom+json"
Example response
[
{
"0020000D": {
"vr": "UI",
"Value": ["1.2.840.113619.2.55.3.604688119.868.1234567890.1"]
},
"00100020": {
"vr": "LO",
"Value": ["PATIENT001"]
},
"00100010": {
"vr": "PN",
"Value": [{ "Alphabetic": "DOE^JOHN" }]
},
"00080020": {
"vr": "DA",
"Value": ["20240101"]
},
"00080030": {
"vr": "TM",
"Value": ["120000"]
},
"00080050": {
"vr": "SH",
"Value": ["ACC001"]
},
"00080061": {
"vr": "CS",
"Value": ["CT"]
},
"00081030": {
"vr": "LO",
"Value": ["Example Study Description"]
},
"00201206": {
"vr": "IS",
"Value": [1]
},
"00201208": {
"vr": "IS",
"Value": [100]
}
}
]
Reading the Warning header
A search naming an unsupported parameter and more matches than fit on one page succeeds, but carries two warnings:
curl -i "http://localhost:3000/studies?PatientSex=F&StudyDate=20240101-20240131" [ \
-H 'Authorization: Bearer <jwt-token>' ]
HTTP/1.1 200 OK
Content-Type: application/dicom+json
Warning: 299 kastoria-dicomweb: The following query parameters are not supported and were ignored: PatientSex
Warning: 299 kastoria-dicomweb: There are more additional results that can be requested
PatientSex is returned in the payload but cannot be matched on, so the
result set is wider than the query asked for; and further pages remain.
Notes for client implementers
- Do not treat
200 (OK)with an empty array as definitive without checking for aWarningheader field. A search limited to a window returns the same empty payload whether nothing matches at all or nothing matches recently. The header is the only distinction. - Treat a
299warning as information, not an error. It accompanies a successful response and does not indicate a failed or degraded search. A response may carry more than one — an ignored parameter, a limited window, and further pages available are independent conditions and can all apply at once. - To search a longer history, page by date rather than widening the range.
Take the date the warning names, and issue the next request with a
StudyDaterange ending the day before it. Repeat until a response arrives without the warning, or until far enough back to stop. Each request is bounded, and together they cover the history without any single request being expensive. - Supply a
StudyDatewhenever one is known. It bounds every attribute at once, which is both faster and the surest way to receive a complete answer rather than a limited one. - Prefer
StudyInstanceUIDwhen it is known. It is answered as a direct lookup, without reading any index.
See also
- JWT Authorization — bearer-token authentication and study scoping of this interface
- STOW-RS — Store Instances — how studies are stored before they become searchable
- WADO-RS — Retrieve Instances — fetch the studies and series a search returns