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
- Create the outgoing connection
- Invoke your first operation
- Build richer requests
- Read responses
- Handle faults
- Deploy with enmasse
Remember: you can connect your AI copilot to Zato documentation.
Create the outgoing connection
- If you do not have Zato running yet, install it via Docker - it takes under 5 minutes.
- Open the web admin dashboard at http://localhost:8183 and navigate to Connections > Outgoing > SOAP.
- 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. - In the SOAP tab, set SOAP version to
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.
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.
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_}
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