Ingestion Manifest (Study Notification Events)
Each time Kastoria Health processes a DICOM imaging study, it publishes a Notification Event carrying the study's Ingestion Manifest: the set of DICOM and resource identifiers a third party needs to map the study between systems. Events are delivered to an Azure Storage Queue in your tenant, one event per message.
This page documents the feature from a consumer's perspective: when events are published, what an event contains, how the queue is set up and secured, and how a consumer reads and applies events.
| Transport | Message format | Delivery | Ordering |
|---|---|---|---|
| Azure Storage Queue | One JSON event per message | At least once | None — order by eventTime |
The ingestion pipeline
Studies are ingested through the STOW-RS endpoint. A study received there passes through the ingestion pipeline, a sequence of steps that each scale independently:
- Store. The DICOM instances are committed to Kastoria Health's versioned storage.
- Process. The stored study is prepared for use. Kastoria Health indexes it for DICOMweb query and retrieval, records its FHIR Patient resource, and builds the summary data that the Ingestion Manifest is drawn from.
A study is considered ingested once every step of the pipeline has completed for it. Kastoria Health may add steps in the future.
Events are not a searchability signal
Notification Events do not wait for the whole pipeline. A study's event is published as soon as Process has built the study's summary, so the DICOMweb indexes for it can still be building when the event arrives. An event confirms what it carries about the study; it is not a signal that the study can yet be queried or retrieved over DICOMweb (see Searchability: stored is not searchable).
Data quality
DICOM metadata fields are often created automatically by the devices or entered manually by the technologist, and the DICOM standard makes many fields optional. Kastoria Health publishes these values as received. It does not correct, normalize or reconcile them. Rigorous data extraction and data quality validation are essential before migrating data to Kastoria Health, so that the data ingested is of high quality and the mapping between systems is clean.
When events are published
Kastoria Health publishes a Notification Event once a study has been processed:
| Event type | Published |
|---|---|
created |
When the study is first processed |
updated |
Every time the study is processed again after that — for example, when further instances or series for the study arrive |
Each event carries the whole Ingestion Manifest as it stood after that change, never only what changed. The consumer can act on an event without calling Kastoria Health back.
- An
updatedevent can carry a manifest identical to the previous one. A change such as a new series of images changes the stored study without necessarily changing any Ingestion Manifest field. - Reprocessing that produces no change publishes nothing. Kastoria Health publishes only when the stored study actually changes.
- A study that has not been processed has no event. A study still being processed, or one that could not be processed, is published once processing succeeds.
- Events are published in periodic runs, so an event reaches the queue shortly after the study is processed, within the publishing interval configured for the deployment.
- Publishing starts from the beginning. When publishing is first enabled for a deployment, events are published for every study already processed, including an event for each earlier change to it.
- Delivery is at least once. The same event can be delivered more than once, and events can arrive in a different order from the one in which the changes happened. Applying events in order gives the one rule a consumer applies to handle both.
The Notification Event
Every event is one JSON object (UTF-8). A single event is about 1–3 KB.
{
"eventId": "bafyreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy",
"eventTopic": "study",
"eventType": "updated",
"eventTime": "2026-09-13T15:04:05.123Z",
"data": {
"imagingStudyResourceID": "img-study-e5f6g7h8",
"studyInstanceUID": "1.2.840.113619.2.55.3.604688119.971.1326810290.945",
"patientResourceID": "pat-a1b2c3d4",
"patientID": "MRN0012345",
"patientName": "Doe^Jane",
"patientBirthDate": "19700101",
"patientSex": "F",
"accessionNumber": "ACC20260913001",
"studyDate": "20260913",
"studyTime": "150405.123456",
"studyDescription": "CT CHEST W CONTRAST",
"studyModalities": ["CT", "SR"],
"referringPhysicianName": "Smith^John^^Dr."
}
}
All keys are camelCase, with acronyms in upper case (studyInstanceUID,
patientID).
Consumers must ignore keys they do not recognize. Kastoria Health may add
keys to the event or to data. There is no version or schema-version field;
an added key is not a breaking change.
Event fields
| Field | Type | Description |
|---|---|---|
eventId |
string | Identifies this event. It is the same on every delivery of the same event, so it can be used to recognize a duplicate and to refer to one event, in logs or in a support request, without quoting patient data. Treat it as opaque |
eventTopic |
string | What the event is about. Always study today |
eventType |
string | What happened to the study. created: the study was processed for the first time. updated: the study was processed again |
eventTime |
string | When the change happened in Kastoria Health, as ISO 8601 UTC with milliseconds (2026-09-13T15:04:05.123Z). This is the time the study was processed, not the time the event was published and not the DICOM Study Date. It is the key events are ordered by (see Applying events in order) |
data |
object | The study's Ingestion Manifest as it stood after the change. See Ingestion Manifest fields |
Event types
Kastoria Health does not currently delete studies, so created and updated
are the only event types published. Two further types are reserved for when
study deletion is introduced:
| Event type | Status | Description |
|---|---|---|
created |
Published | The study was processed for the first time |
updated |
Published | The study was processed again |
removed |
Reserved | data is the study's last manifest before it was removed |
restored |
Reserved | — |
The definitions of the reserved types will be confirmed before either is first published.
Ingestion Manifest fields (data)
Each field is present in every event. A value that is absent from the DICOM
data is null, except studyModalities, which is always an array.
| Field | Type | Description |
|---|---|---|
imagingStudyResourceID |
string | The FHIR ImagingStudy id Kastoria Health assigns to the study. It is assigned when the study is first processed and does not change afterwards. It is the id an EHR passes as imagingStudy in a SMART on FHIR launch (see SMART on FHIR — smart-launch-api). There is no separate ImagingStudy resource behind it |
studyInstanceUID |
string | DICOM Study Instance UID (0020,000D). The unique identifier of the study. Events are applied in order per study, so this is the key the eventTime comparison is made within |
patientResourceID |
string or null |
The id of the FHIR Patient resource Kastoria Health holds for the study's patient. null when the study carries no Patient ID, since such a study has no Patient resource |
patientID |
string or null |
DICOM Patient ID (0010,0020) |
patientName |
string or null |
DICOM Patient's Name (0010,0010), as a person name |
patientBirthDate |
string or null |
DICOM Patient's Birth Date (0010,0030), as a date |
patientSex |
string or null |
DICOM Patient's Sex (0010,0040), as recorded, normally M, F or O |
accessionNumber |
string or null |
DICOM Accession Number (0008,0050) |
studyDate |
string or null |
DICOM Study Date (0008,0020), as a date |
studyTime |
string or null |
DICOM Study Time (0008,0030), as a time |
studyDescription |
string or null |
DICOM Study Description (0008,1030) |
studyModalities |
array of strings | The modalities in the study (e.g. CT, MR, XR), collected from the Modality (0008,0060) of each series, in upper case, each listed once, in no particular order. [] when no series records a modality |
referringPhysicianName |
string or null |
DICOM Referring Physician's Name (0008,0090), as a person name |
Patient and study values are taken from the study's DICOM instances as received.
DICOM value encoding
DICOM values are carried as stored, in their DICOM encoding, with leading and trailing spaces removed:
| VR | Format | Example |
|---|---|---|
| Date (DA) | YYYYMMDD |
19700101 |
| Time (TM) | HHMMSS.FFFFFF; as DICOM allows, trailing components may be omitted |
150405.123456, 150405, 1504 |
| Person name (PN) | DICOM alphabetic representation, components separated by carets: Family^Given^Middle^Prefix^Suffix; trailing empty components may be omitted |
Doe^Jane |
Queue
Kastoria Health publishes Notification Events to an Azure Storage Queue. Each queue message is exactly one event.
The event structure does not depend on the queue. Everything a consumer needs is in the JSON body, and nothing is carried in queue-specific message properties. If a different queue technology is agreed, the event is unchanged.
Queue properties
| Property | Value |
|---|---|
| Message expiry | Never. Every message is sent with no expiry (a time-to-live of -1), so an unread message stays on the queue until it is deleted. This overrides Azure's 7-day default |
| Message body encoding | text (the JSON as-is) or base64 (the JSON's UTF-8 bytes, base64-encoded), configurable per deployment. Every message on a queue uses the same encoding. An Azure Functions queue trigger expects base64 by default |
| Maximum message size | 64 KiB, as set by Azure. An event is a small fraction of that in either encoding |
| Delivery | At least once |
| Ordering | None. Order by eventTime (see Applying events in order) |
| Duplicate detection | None. Duplicates are expected, and the ordering rule makes them harmless |
| Dead-letter queue | None. See Messages that cannot be handled |
| Replay | None. A message deleted from the queue is gone. See Recovery |
Queue location
Kastoria Health is deployed in your Azure tenant, so the Storage Account and its queue are your resources. You create the queue and grant access to it. Kastoria Health does not create the queue. The queue can be in either of two places:
- In the Storage Account deployed with Kastoria Health. Create the queue in the Storage Account that is part of Kastoria Health's infrastructure.
- In your own Storage Account (optional). Create an Azure Storage Queue in a Storage Account of your choosing, and name that account in the Kastoria Health configuration.
Access
Access in both directions uses Entra ID role assignments on the queue. No SAS token is issued, by you or by Kastoria Health, so there is no queue credential to distribute, rotate or revoke; access is withdrawn by removing the role assignment.
| Party | Authenticates as | Role on the queue | Permits |
|---|---|---|---|
| Kastoria Health (publisher) | Its managed identity | Storage Queue Data Message Sender | Adding messages only — neither reading them nor changing the queue |
| Your consumer | An Entra ID principal of its own — a managed identity where it runs in Azure, a service principal where it does not | Storage Queue Data Message Processor | Reading, receiving and deleting messages — neither adding them nor changing the queue |
Kastoria Health authenticates to the queue the same way it reaches every other Azure Storage resource it uses, so no queue credential is configured or held anywhere. Once its identity holds the sender role, configure Kastoria Health with:
- the Storage Account name
- the queue's name
- the message encoding (
textorbase64)
All access is over HTTPS.
Every message contains protected health information (patient name, birth date and identifiers). Handle messages, logs and anything built from them accordingly.
Reading events
A consumer can use any Azure Storage Queue client: the Azure SDKs for .NET, Java, Python and JavaScript, an Azure Functions queue trigger, or the Queue service REST API directly. Each authenticates as the Entra ID principal described in Access:
| Client | Authentication |
|---|---|
| Azure SDKs | DefaultAzureCredential |
| Azure Functions queue trigger | Identity-based connection |
| Queue service REST API | Entra ID access token in the Authorization header |
The receive loop
- Receive a batch of messages (Get Messages, up to 32 at a time), with a visibility timeout long enough to handle the whole batch. While a message is invisible, no other receiver gets it.
- For each message:
- Decode the body according to the configured encoding, and parse it as JSON.
- Apply the event to your system using the rule in Applying events in order, and make that change durable.
- Delete the message (Delete Message, with the message id and the pop receipt from the receive).
- When the queue is empty, wait before receiving again, then repeat.
Delete a message only after its event is durably applied. If the consumer stops between applying and deleting, the message becomes visible again when its visibility timeout ends, and is delivered a second time. The ordering rule makes that harmless. The reverse order, deleting first, would lose the event if the consumer stopped before applying it.
With an Azure Functions queue trigger, the Functions runtime receives and
deletes for you. The message is deleted when the function completes
successfully. If Kastoria Health sends text, set the queue trigger's
messageEncoding to none in host.json.
Applying events in order
For each study, identified by data.studyInstanceUID:
Apply an event when its
eventTimeis equal to or later than theeventTimeof the event you hold for that study. Otherwise, discard it.
- Order by
eventTime, never by arrival. Events for one study can arrive out of order, and an older event can arrive after a newer one. - No separate duplicate check is needed. A duplicate carries the same
eventId,eventTimeanddataas the original, so applying it again changes nothing. UseeventIdto recognize one if you want to count or log duplicates. - Keep the
eventTimeyou applied with the study, so later events can be compared against it. - Make the comparison and the write one step if more than one consumer
instance writes to your system at once. Otherwise two instances can each
read the old
eventTimeand the older event can be written last.
Concurrent consumers
Several instances of the same consumer may receive from one queue concurrently to increase throughput. The rule above keeps the result correct.
Two different applications must not read the same queue. A Storage Queue gives each message to one receiver, so each application would see only part of the stream. Handle every purpose in one consumer.
Messages that cannot be handled
Messages never expire and the queue has no dead-letter queue, so a message the
consumer cannot handle returns to the queue after every visibility timeout,
indefinitely. Each message carries a dequeue count. Decide on a limit, and
when a message exceeds it, move the message somewhere it can be investigated
(a -poison queue or a table, for example) before deleting it. An Azure
Functions queue trigger does this automatically, moving the message to a
queue named <queue-name>-poison after five attempts by default.
When reporting a problem message to Kastoria Health, quote its eventId, not
its contents.
Recovery
A message deleted from the queue cannot be read again. If a consumer loses
events it had already deleted, an operator can re-publish every event from
a point in time they specify, from the Kastoria Health admin console.
Re-published events carry the same eventId, eventTime and data as the
originals, so the ordering rule applies them safely over whatever your system
already holds.
Examples
Create the queue and grant access
Create the queue, then grant the sender role to Kastoria Health's managed identity and the processor role to your consumer's principal, both scoped to the queue:
az storage queue create \
--account-name {storage-account} \
--name {queue} \
--auth-mode login
QUEUE_SCOPE="/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.Storage/storageAccounts/{storage-account}/queueServices/default/queues/{queue}"
az role assignment create \
--assignee {kastoria-managed-identity-principal-id} \
--role "Storage Queue Data Message Sender" \
--scope "$QUEUE_SCOPE"
az role assignment create \
--assignee {consumer-principal-id} \
--role "Storage Queue Data Message Processor" \
--scope "$QUEUE_SCOPE"
Azure Functions trigger for text encoding
When Kastoria Health is configured to send text, disable the trigger's
default base64 decoding in host.json:
{
"version": "2.0",
"extensions": {
"queues": {
"messageEncoding": "none"
}
}
}
Applying an out-of-order event
Two events for the same study arrive in reverse order. The consumer holds no event for the study when the first one arrives:
| Arrival | eventType |
eventTime |
Held eventTime |
Action |
|---|---|---|---|---|
| 1 | updated |
2026-09-13T15:04:05.123Z |
— | Apply; hold 15:04:05.123Z |
| 2 | created |
2026-09-13T14:58:12.004Z |
15:04:05.123Z |
Discard — earlier than held |
| 3 | updated (duplicate of 1) |
2026-09-13T15:04:05.123Z |
15:04:05.123Z |
Apply — equal; no change |
See also
- STOW-RS — Store Instances — the endpoint studies are ingested through
- QIDO-RS — Search for Instances — note that a study announced by an event may not yet be searchable
- SMART on FHIR — smart-launch-api
— launches the viewer with the
imagingStudyResourceIDan event carries