# SOAP Tutorial - Accept SOAP requests without learning SOAP

A channel unwraps incoming envelopes into plain objects and your service answers with another - document submission as the running example.

A partner's system, a hospital or a government network needs to send SOAP requests to you, and their side is fixed: they will send envelopes, expect response elements and treat anything malformed as an error.

This tutorial shows how to be that endpoint with nothing but Python objects. A SOAP channel unwraps each incoming envelope before your service runs, the service reads the request with dot access and responds by assigning another object - no XML, no schemas, no WSDL compilation.

The running example is a healthcare document repository of the kind IHE document submission profiles prescribe - a system that accepts binary documents over SOAP and answers with a registry response. Everything shown here applies unchanged to any other SOAP interface you need to expose.

## In this tutorial {#in-this-tutorial}

1. [Create the channel](https://zato.io/tutorials/soap/receiving.html#create-the-channel)
2. [Read the request](https://zato.io/tutorials/soap/receiving.html#read-the-request)
3. [Respond with an object](https://zato.io/tutorials/soap/receiving.html#respond-with-an-object)
4. [Reject requests with faults](https://zato.io/tutorials/soap/receiving.html#reject-requests-with-faults)
5. [Receive documents with MTOM](https://zato.io/tutorials/soap/receiving.html#receive-documents-with-mtom)
6. [Read the protocol context](https://zato.io/tutorials/soap/receiving.html#read-the-protocol-context)
7. [Deploy with enmasse](https://zato.io/tutorials/soap/receiving.html#deploy-with-enmasse)

> **Remember:** you can [connect your AI copilot](https://zato.io/docs/ai/connect-your-copilot.html) to Zato documentation.

## Create the channel {#create-the-channel}

**Step 1.** If you do not have Zato running yet, install it via [Docker](https://zato.io/docs/admin/guide/install/docker.html) - it takes under 5 minutes.

**Step 2.** Open the web admin dashboard at **http://localhost:8183**, navigate to **Connections** > **Channels** > **SOAP**, click **Create a new SOAP channel** and fill in the form:

- **Name:** `Document Repository`
- **URL path:** `/xds/repository`
- **Service:** the service that will handle the requests - you will deploy it in a moment, so any existing service works for now and you can change it later through Edit
- **SOAP action:** `urn:ihe:iti:2007:ProvideAndRegisterDocumentSet-b`
- **SOAP version:** `1.2`
- Click **OK**

Every field has a **How does it work?** link next to it that explains what the field does and when you need it.

From this moment, any SOAP client that posts an envelope to `/xds/repository` reaches your service - the channel takes care of parsing the envelope, whichever SOAP version and packaging the caller uses.

## Read the request {#read-the-request}

Open the Zato IDE at **http://localhost:8183**, create a new file called `repository_api.py`, paste this code, click **Deploy**, and then assign the service to the channel through the channel's Edit dialog:

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

# Zato
from zato.server.service import Service

# ################################################################################################################################
# ################################################################################################################################

class ProvideAndRegisterDocumentSet(Service):
    """ Accepts document submissions of a healthcare document exchange.
    """

    name = 'repository-api.provide-and-register'

    def handle(self) -> 'None':

        # The channel already unwrapped the envelope - this is the operation
        # element from the message body, as a dot-accessed object.
        request = self.request.payload

        # Fields read by their names, no namespaces required
        patient_id = request.patientID
        comments = request.comments

        self.logger.info('Received a submission for %s: %s', patient_id, comments)
```

This code does not refer to the envelope, header or body. The service receives the operation element - the single child of the request's body - and reads its fields with the dot access that [outgoing calls](https://zato.io/tutorials/soap/calling.html) use for responses: nested elements are deeper dot access, repeated elements are lists, XML attributes go through brackets.

Services that want the raw bytes instead can still read `self.request.raw` - unwrapping is additive, nothing is taken away.

> **Try it:** Post any SOAP envelope to **http://localhost:11223/xds/repository** with curl or any SOAP client - the service logs the fields it found.

## Respond with an object {#respond-with-an-object}

A response uses the same message objects - build one and assign it to `self.response.payload`:

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

# Zato
from zato.common import SOAPMessage
from zato.server.service import Service

# ################################################################################################################################
# ################################################################################################################################

class ProvideAndRegisterDocumentSet(Service):

    name = 'repository-api.provide-and-register'

    def handle(self) -> 'None':

        request = self.request.payload

        self.logger.info('Received a submission for %s', request.patientID)

        # Build the reply - the channel wraps it in an envelope of the request's
        # SOAP version, inside the operation's response element.
        response = SOAPMessage()
        response.status = 'urn:ihe:iti:2007:ResponseStatusType:Success'

        self.response.payload = response
```

The caller receives a well-formed `ProvideAndRegisterDocumentSetResponse` element inside an envelope matching their request - the service never chooses the SOAP version, never names the response element and never handles the reply headers.

When the request included WS-Addressing, the reply automatically includes the addressing headers a conformant caller expects: a `wsa:Action` derived from the request's, a fresh `wsa:MessageID` and `wsa:RelatesTo` echoing the request's message id.

## Reject requests with faults {#reject-requests-with-faults}

A rejected request is an ordinary Python exception - the channel converts it to a SOAP fault of the request's version, with the right fault code for each dialect:

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

# Zato
from zato.common import SOAPMessage
from zato.common.exception import BadRequest
from zato.server.service import Service

# ################################################################################################################################
# ################################################################################################################################

class ProvideAndRegisterDocumentSet(Service):

    name = 'repository-api.provide-and-register'

    def handle(self) -> 'None':

        request = self.request.payload

        # This becomes a Sender fault with this exact message
        if not request.patientID:
            raise BadRequest(self.cid, 'patientID is required')

        response = SOAPMessage()
        response.status = 'urn:ihe:iti:2007:ResponseStatusType:Success'

        self.response.payload = response
```

Client errors - `BadRequest` and its relatives - become Sender faults with the exception's message as the fault reason. Any other exception becomes a Receiver fault with a generic message, and a Python traceback never reaches the wire either way.

> **Try it:** Post an envelope without a `patientID` element - the fault comes back as `soap:Sender` with SOAP 1.2 and as `soap:Client` with SOAP 1.1, matching whatever the request was.

## Receive documents with MTOM {#receive-documents-with-mtom}

Document submissions contain binary content, and document-exchange profiles require it to be sent as MTOM parts rather than inline base64. On the receiving side this is invisible - a field the caller sent as an MTOM part reads back as plain `bytes`:

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

# stdlib
from hashlib import sha256

# Zato
from zato.common import SOAPMessage
from zato.server.service import Service

# ################################################################################################################################
# ################################################################################################################################

class ProvideAndRegisterDocumentSet(Service):

    name = 'repository-api.provide-and-register'

    def handle(self) -> 'None':

        request = self.request.payload

        # An MTOM part is read as bytes, the same as a value the sender inlined
        document = request.Document

        self.logger.info('Received a document of %d bytes', len(document))

        # Bytes are assigned like any other value - with MTOM enabled on the channel,
        # the receipt leaves as an MTOM package, otherwise as inline base64.
        response = SOAPMessage()
        response.status = 'urn:ihe:iti:2007:ResponseStatusType:Success'
        response.receipt = sha256(document).digest()

        self.response.payload = response
```

For the response direction, tick the **MTOM** checkbox on the channel - from then on, any `bytes` value your service assigns leaves as an MTOM part. The raw MIME parts of the request are also available, as `self.request.soap.attachments`, for services that want to inspect content ids or content types.

## Read the protocol context {#read-the-protocol-context}

Everything the channel established about the request is available in `self.request.soap`, next to the payload and never mixed into it:

```python
soap = self.request.soap

# The version and operation of the incoming request
self.logger.info('SOAP %s, operation %s', soap.soap_version, soap.operation)

# The request's WS-Addressing headers, when it had any
self.logger.info('wsa:MessageID %s', soap.addressing.message_id)

# What security enforcement established - e.g. the verified username
# of a UsernameToken-protected channel
self.logger.info('Verified user: %s', soap.security.username)

# The raw envelope bytes, for services that need them
self.logger.info('Envelope of %d bytes', len(soap.envelope))
```

Security itself is configured on the channel, not read in code - attach a [WS-Security definition](https://zato.io/docs/dev/soap/security/index.html) through the channel's **Security** dropdown and the platform rejects any request that does not satisfy it, with the correct fault, before your service ever runs.

## Deploy with enmasse {#deploy-with-enmasse}

Everything you configured through the Dashboard can also be defined declaratively in YAML and deployed with enmasse. This is the recommended approach for production, CI/CD pipelines, and version-controlled infrastructure.

The channel from this tutorial can be expressed as:

```yaml
channel_soap:

  - name: Document Repository
    service: repository-api.provide-and-register
    url_path: /xds/repository
    soap_action: urn:ihe:iti:2007:ProvideAndRegisterDocumentSet-b
    soap_version: "1.2"
    use_mtom: true
```

Import it in the Dashboard under `System → Config → Import enmasse`, or mount the file under `/opt/hot-deploy/enmasse/enmasse.yaml` inside the container to have it imported on start.

## What you built {#what-you-built}

- A **SOAP channel** that accepts envelopes of either SOAP version, bare or multipart, on the URL path you chose
- A **service** that reads requests through dot access and never parses XML
- **Object responses** - the channel wraps them in envelopes matching each request, WS-Addressing reply headers included
- **Faults from exceptions** - client errors as Sender faults with their message, everything else as Receiver faults with no internal details exposed
- **MTOM in both directions** - incoming parts as `bytes`, outgoing `bytes` as parts
- **Enmasse deployment** - declarative YAML for automated, repeatable provisioning

Continue with the [SOAP security tutorial](https://zato.io/tutorials/soap/security.html) to protect the channel with [WS-Security](https://zato.io/docs/security/ws-security.html), or read the [SOAP integrations](https://zato.io/docs/dev/soap/index.html) pillar for every building block: WS-Addressing, MTOM attachments, ebXML and more.
