SOAP Tutorial - Call any SOAP service with Python objects

Requests as plain Python objects, dot-accessed responses and fault handling - with an immunization registry as the running example.

Public health registries, ERPs and banking backends are commonly reached over SOAP. This tutorial shows how to call them from Zato with nothing but Python objects: no XML strings, no schemas, no WSDL compilation.

The running example is an immunization registry of the kind operated by U.S. states - their interface follows the CDC's WSDL for immunization information systems, with operations such as connectivityTest and submitSingleMessage. Everything shown here applies unchanged to any other SOAP service.

Call your first SOAP operation in under 5 minutes.

In this tutorial

  1. Create the outgoing connection
  2. Invoke your first operation
  3. Build richer requests
  4. Read responses
  5. Handle faults
  6. Deploy with enmasse

Remember: you can connect your AI copilot to Zato documentation.

Create the outgoing connection

  1. If you do not have Zato running yet, install it via Docker - it takes under 5 minutes.
  2. Open the web admin dashboard at http://localhost:8183 and navigate to Connections > Outgoing > SOAP.
  3. Click Create a new outgoing SOAP connection and fill in the form - Name: Immunization Registry, Host: https://registry.example.gov, URL path: /iisb/services, SOAP action: urn:cdc:iisb:2011:connectivityTest.
  4. In the SOAP tab, set SOAP version to 1.2.
  5. 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.

Note: Each SOAP action gets its own connection - if the registry exposes connectivityTest and submitSingleMessage, that is two connections differing only in name and SOAP action.

Invoke your first operation

Open the Zato IDE at http://localhost:8183, create a new file called registry_api.py, paste this code, and click Deploy:

# -*- coding: utf-8 -*-

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

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

class ConnectivityTest(Service):
    """ Checks that the immunization registry is reachable and answering.
    """

    name = 'registry-api.connectivity-test'

    def handle(self) -> 'None':

        # Build the request - one field, echoBack, which the registry echoes in its reply
        request = SOAPMessage()
        request.namespace = 'urn:cdc:iisb:2011'
        request.echoBack = 'Hello registry'

        # Invoke the operation over the connection configured in the Dashboard
        conn = self.soap['Immunization Registry']
        response = conn.invoke('connectivityTest', request)

        # The reply mirrors the request's shape - dot access all the way down
        echoed = response.connectivityTestResponse.return_

        self.response.payload = {'echoed': echoed}

That is the whole flow: build a message, invoke an operation, read the reply. The platform wraps your fields in the right envelope for the configured SOAP version, sets the SOAP action, sends the request and parses what comes back.

You can invoke the service straight from the IDE's invoker panel to watch it run.

Try it: Change echoBack to another value, redeploy and invoke again - the registry echoes back whatever you send.

Build richer requests

Attributes become elements, in the exact order you assign them, and nesting is just deeper dot access. Submitting an HL7 message to the registry looks like this:

request = SOAPMessage()
request.namespace = 'urn:cdc:iisb:2011'
request.hl7Message = 'MSH|^~\\&|MYAPP|MYFAC|IIS|STATE|20260115||VXU^V04^VXU_V04|123|P|2.5.1'

response = self.soap['Registry Submit'].invoke('submitSingleMessage', request)

A few more shapes you will need sooner or later:

# Nested elements - intermediate levels spring into existence
request.patient.name.family = 'Smith'
request.patient.name.given = 'Jane'

# XML attributes go through brackets
request.slot['name'] = 'creationTime'

# Lists become repeated elements
request.codes.code = ['90715', '90716']

# Python types choose the XML form - datetime becomes xs:dateTime,
# bool becomes true/false, None becomes xsi:nil (from datetime import datetime)
request.submittedOn = datetime.utcnow()
request.isProduction = False

The full set of rules is in the Python message objects reference.

Read responses

Responses are the same kind of object, so reading mirrors writing. The reply's fields live under the operation's response element - submitSingleMessageResponse for submitSingleMessage:

response = conn.invoke('submitSingleMessage', request)

# Scalar fields are plain strings
ack = response.submitSingleMessageResponse.return_

# Repeated elements are lists
for detail in response.submitSingleMessageResponse.details.detail:
    self.logger.info('Detail: %s', detail)

# Attributes read through brackets
status_code = response.submitSingleMessageResponse.status['code']

No namespaces are ever needed to read a reply - dot access finds elements by their local names.

Handle faults

When the endpoint rejects a request, it answers with a SOAP fault - and a fault of either SOAP version surfaces as the one SOAPFault exception:

# -*- coding: utf-8 -*-

# Zato
from zato.common.soap.common import SOAPFault
from zato.common.soap.message import SOAPMessage
from zato.server.service import Service

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

class SubmitWithFallback(Service):

    name = 'registry-api.submit-with-fallback'

    def handle(self) -> 'None':

        request = SOAPMessage()
        request.namespace = 'urn:cdc:iisb:2011'
        request.hl7Message = self.request.raw_request

        try:
            response = self.soap['Registry Submit'].invoke('submitSingleMessage', request)
        except SOAPFault as fault:

            # fault.code is Sender or Receiver, fault.reason is the human-readable text
            # and fault.detail is a dot-accessed message with whatever the endpoint added
            self.logger.warning('Registry rejected the message: %s %s', fault.code, fault.reason)
            self.response.status_code = 502

        else:
            self.response.payload = {'ack': response.submitSingleMessageResponse.return_}
Try it: Point the connection at a URL path that does not exist and invoke the service - the resulting fault contains the endpoint's error text in fault.reason.

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 connection from this tutorial can be expressed as:

outgoing_soap:

  - name: Immunization Registry
    host: https://registry.example.gov
    url_path: /iisb/services
    soap_action: urn:cdc:iisb:2011:connectivityTest
    soap_version: "1.2"
    timeout: 30

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

  • An outgoing SOAP connection holding the endpoint's address, SOAP action and version
  • A service that builds requests as plain Python objects and invokes operations with self.soap['name'].invoke(...)
  • Dot-accessed responses - nested fields, lists, and attributes, no namespaces required
  • Fault handling with a single exception type instead of parsed XML
  • Enmasse deployment - declarative YAML for automated, repeatable provisioning

Continue with the SOAP security tutorial - the registry example gets real credentials there - or read the SOAP integrations pillar for every building block: WS-Addressing, MTOM attachments, ebXML and more.


Schedule a meaningful demo

Book a demo with an expert who will help you build meaningful systems that match your ambitions

"For me, Zato Source is the only technology partner to help with operational improvements."

- John Adams
Program Manager of Channel Enablement at Keysight