# IHE XDM document packages

Read IHE XDM packages - the zip with IHE\_XDM and METADATA.XML that Direct messages, SFTP drops and media carry clinical documents in - and get each document with its metadata, in Python.

An IHE XDM package - Cross-Enterprise Document Media Interchange - is a zip archive that carries clinical documents together with a description of each of them, which is how documents travel when there is no document registry between the sender and the recipient - in a Direct message, on a USB stick or a CD handed to a patient, or as a file dropped on an SFTP server. Zato opens the package and gives your service each document the package describes, with its metadata, in one call.

Inside the zip, an `IHE_XDM` directory holds one subdirectory per submission set - the documents one sender sent together - and each subdirectory holds the documents and a `METADATA.XML` file, an ebXML `SubmitObjectsRequest` with an entry per document stating its location, MIME type, title, patient, document codes, hash and size. Next to the directory, the root of the zip has an `INDEX.HTM` and a `README.TXT` for a person opening the package by hand, which the reading skips.

## Read a package {#read-a-package}

A service is a Python class that Zato runs when something invokes it. Whatever brought the package to the service - an email attachment, a file, a REST request - the service reads the documents inside it the same way:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class XDMImport(Service):
    """ Stores each clinical document of an XDM package in a FHIR server.
    """
    name = 'documents.xdm-import'

    def handle(self) -> 'None':

        # Each document the package's metadata names, in metadata order ..
        for document in self.request.input.documents():

            self.logger.info('%s -> %s, %s, patient %s', document.file_name, document.title, document.mime_type,
                document.patient_id)

            # .. the human-readable renderings that travel alongside are not converted ..
            if document.mime_type == 'application/pdf':
                continue

            # .. and each clinical document becomes a FHIR bundle that goes to the server as a whole.
            bundle = self.ccda.to_fhir(document.data)

            client = self.fhir['FHIR.Registry']
            client.execute('', method='post', data=bundle.to_dict())
```

Each document has these attributes:

| Attribute | What it holds |
| --- | --- |
| `data` | The document's bytes, as they are in the package |
| `file_name` | The document's file name inside the package |
| `mime_type` | Its MIME type, from the metadata |
| `size` | The size of the document, in bytes |
| `source` | `xdm` for a document read out of a package, `zip` for a file of a plain archive and `attachment` for a document that arrived on its own |
| `title` | The document's title |
| `patient_id` | The patient the document is about, as the sender's system identifies them |
| `class_code` | The document's class code, e.g. `34133-9` for a summary of episode note |
| `type_code` | The document's type code |
| `unique_id` | The document's unique ID, the same wherever the document goes |
| `creation_time` | When the document was created, as the metadata states it |
| `language` | The document's language code, e.g. `en-US` |

The call opens the package, reads the metadata of every submission set and returns the documents the metadata names - a file in the package that no entry describes, a thumbnail or a note, is left out. Each document's size and SHA-1 hash are checked against what the metadata states about it, and a document that does not match raises an exception before anything is returned, so a service never acts on a package that was changed in transit.

The call is not specific to XDM. An attachment or a file that is not a zip is returned as the one document it is, with its MIME type and no metadata, and a zip that has no `IHE_XDM` directory returns each file inside it as a document with `source` set to `zip` - so a service written as the one above handles a bare C-CDA, a plain zip of documents and an XDM package without telling them apart.

## Where packages come from {#where-packages-come-from}

The package reaches the service in one of several ways, and in each the service calls `documents()` on what it received:

1. **A Direct message.** The package is the attachment of an email, and an [IMAP connection](https://zato.io/docs/dev/healthcare/direct/index.html) with its Scheduler section set to **Each attachment** invokes the service once per attachment, so `self.request.input` is the attachment. A message whose subject starts with `XDM/1.0/DDM` carries a package.
2. **A file.** A [file transfer](https://zato.io/docs/dev/file-transfer/receiving-files.html) channel watches a directory or an SFTP server that a sender drops packages into, and `self.request.input` is the file.
3. **A REST upload.** A channel receives the zip in the request body, and `read_documents` from `zato.common.documents.unpack` reads it:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.common.documents.unpack import read_documents
from zato.server.service import Service

class XDMUpload(Service):
    """ Receives an XDM package over REST and logs the documents it names.
    """
    name = 'documents.xdm-upload'

    def handle(self) -> 'None':

        for document in read_documents(self.request.input, file_name='upload.zip'):
            self.logger.info('%s -> %s', document.file_name, document.title)
```

For the first case, an IMAP connection reading the inbox that packages arrive in is created under **Connections** > **Outgoing** > **E-mail** > **IMAP** in the Dashboard, with the service to invoke and **Each attachment** in its Scheduler row:

[Video: IMAP connection for XDM packages](https://zatosource-production.b-cdn.net/docs/gfx/email/imap-xdm-create.webm?v=1791135993)

Dashboard menu: Connections > Outgoing > E-mail > IMAP

## Metadata {#metadata}

Each document's attributes come from its entry in `METADATA.XML` - an `ExtrinsicObject` with slots, a name, classifications and external identifiers:

| Attribute | Where it comes from |
| --- | --- |
| `file_name` | The `URI` slot, which names the file relative to the metadata, with the submission set's directory |
| `mime_type` | The `mimeType` attribute of the entry |
| `title` | The `Name` element's `LocalizedString` |
| `patient_id` | The `XDSDocumentEntry.patientId` external identifier |
| `class_code` | The `classCode` classification, scheme `urn:uuid:41a5887f-8865-4c09-adf7-e362475b143a` |
| `type_code` | The `typeCode` classification, scheme `urn:uuid:f0306f51-975f-434e-a61c-c59651d33983` |
| `unique_id` | The `XDSDocumentEntry.uniqueId` external identifier |
| `creation_time` | The `creationTime` slot |
| `language` | The `languageCode` slot |
| `size` | Counted from the document, and compared with the `size` slot when the metadata has one |
| Hash | The `hash` slot, compared with the SHA-1 of the document when the metadata has one |

A package may hold several submission sets, each in its own subdirectory with its own metadata, and the documents of all of them are returned, set by set.

## XDR and XDS.b {#xdr-and-xdsb}

The same metadata describes documents in the other IHE document-sharing profiles. XDR - Cross-Enterprise Document Reliable Interchange - sends the documents and their `SubmitObjectsRequest` in a SOAP message rather than a zip, and XDS.b registers them with a document registry that other systems query. Both are SOAP 1.2 with MTOM and the stack that [IHE healthcare document exchange](https://zato.io/docs/dev/soap/mandates/healthcare-ihe/index.html) describes, and XDM is the same documents and metadata with no SOAP and no registry, which is why it is what travels in email and on media.

## Troubleshooting {#troubleshooting}

**The exception says `no-metadata`.** The zip has an `IHE_XDM` directory but no `METADATA.XML` in any of its subdirectories, or the file is there but is not XML the reading can parse. The sender's package is incomplete.

**The exception says `missing-file`.** An entry's `URI` slot names a file that is not in the package. The exception's `file_name` is the name the metadata gave.

**The exception says `hash-mismatch` or `size-mismatch`.** The document in the package is not the one the metadata describes - it was changed after the package was built, or the sender's system states a hash or size for a different version of the document. The message or the file stays where it was, since the service raised, and the sender is the one to ask.

**All files came back as documents, with no titles.** The zip has no `IHE_XDM` directory at its root, so it was read as a plain archive. A package whose `IHE_XDM` is inside another directory is not an XDM package by the profile's definition.

## See also {#see-also}

- [Direct Secure Messaging](https://zato.io/docs/dev/healthcare/direct/index.html) - The messages that most XDM packages arrive in
- [C-CDA to FHIR](https://zato.io/docs/dev/healthcare/hl7/ccda/index.html) - Converting the documents a package holds
- [Receiving files](https://zato.io/docs/dev/file-transfer/receiving-files.html) - Picking packages up from directories and SFTP servers
- [IHE healthcare document exchange](https://zato.io/docs/dev/soap/mandates/healthcare-ihe/index.html) - XDR and XDS.b over SOAP, with MTOM and signed SAML
- [IMAP](https://zato.io/docs/dev/examples/imap.html) - Reading email on a schedule, with messages or attachments on input

## Learn more {#learn-more}

- [Healthcare interface engine](https://zato.io/docs/dev/healthcare/) - Clinical messages, FHIR, conversion and operations
- [HL7 v2 parsing](https://zato.io/docs/dev/healthcare/hl7/v2/parsing/) - Messages as typed objects, with field access and validation
- [MLLP channels](https://zato.io/docs/dev/healthcare/hl7v2/mllp/) - Receiving and sending HL7 v2 over MLLP sockets
- [FHIR integrations](https://zato.io/docs/dev/healthcare/hl7/fhir/) - Read and write FHIR resources, with paths, bundles and extensions
- [HL7 v2 to FHIR](https://zato.io/docs/dev/healthcare/hl7/to-fhir/) - Converting v2 messages into FHIR bundles, codes and references included
- [EDIFACT](https://zato.io/docs/dev/healthcare/edifact/) - Parsing interchanges, dialects and the transports they arrive on
- [Transformation](https://zato.io/docs/dev/healthcare/transformation/) - One message in, another standard out, in ordinary Python
- [Audit log](https://zato.io/docs/dev/healthcare/audit-log.html) - Every message with its acknowledgment, searchable by patient identifier
- [AI in clinical interfaces](https://zato.io/docs/dev/healthcare/ai/) - Services calling LLMs and AI agents calling clinical services as tools
