Python SOAP

How to read a WSDL, create outgoing connections and invoke SOAP operations in Python - with dynamic data, lists and fault handling.

SOAP is a messaging protocol that was popular in the 2000s. Today, it is mostly found in legacy and enterprise systems, such as ERP or banking backends, as well as in healthcare and public-sector networks where it remains the mandated protocol.

If you are familiar with REST and JSON, think of SOAP as an older equivalent:

  • Where REST uses JSON, SOAP uses XML
  • Where REST has OpenAPI specifications, SOAP has WSDL documents
  • Where REST endpoints accept JSON objects, SOAP endpoints accept XML documents called envelopes

In Zato, you never write that XML yourself - you build requests as plain Python objects and read responses the same way, while outgoing SOAP connections take care of envelopes, security and everything else the protocol requires.

How to read a WSDL file

If you have worked with OpenAPI or Postman, you know that an OpenAPI specification describes what endpoints exist, what parameters they accept, and what responses they return. A WSDL file serves the same purpose for SOAP services.

When you receive a WSDL file from a SOAP service provider, look for:

  • Operations - these are like REST endpoints. They have names like Read, Create, or Update and tell you what actions you can perform.
  • Messages - these describe the input and output parameters for each operation, similar to request and response schemas in OpenAPI.
  • Types - these define the data structures, like OpenAPI's component schemas.

For example, if you see an operation called CreatePOHeader that accepts parameters like orderNo, vendor, and amount, you know your request needs those fields inside the operation element - which in Zato means assigning three attributes to a message object, in that order.

You never compile the WSDL or generate code from it - it is documentation you read, nothing your services depend on.

Authentication

SOAP outgoing connections support the following authentication methods:

  • HTTP Basic Auth - standard username and password authentication
  • NTLM - Windows-based authentication, commonly used with Microsoft systems
  • WS-Security - UsernameToken, X.509 signing and encryption, and SAML assertions
  • Body credentials - username and password injected into the request body itself
  • Client certificates - mutual TLS for endpoints that authenticate at the transport layer

You configure these in the security definition dropdown and the Security tab when creating a SOAP outgoing connection - see SOAP security for the details of each method.

Invoking a SOAP endpoint

First, create an outgoing SOAP connection. Each SOAP action requires its own outgoing connection - if you need to call three different operations, you create three separate connections.

The SOAP action field is specific to each operation. You find these values in the WSDL file under <soap:operation soapAction="..."> elements. Examples:

  • Reading a sales order: urn:microsoft-dynamics-schemas/page/salesorder:Read
  • Creating a sales order: urn:microsoft-dynamics-schemas/page/salesorder:Create
  • Creating a purchase order header: urn:microsoft-dynamics-schemas/codeunit/PurchaseOrderWebService:CreatePOHeader

Here is an example of a SOAP outgoing connection:

Now, you can invoke it from a service, like this:

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

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

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

class ReadSalesOrder(Service):

    name = 'example.soap.sales-order.read'

    def handle(self) -> 'None':

        # Build the request - each attribute becomes an element
        request = SOAPMessage()
        request.namespace = 'urn:example-schemas/page/salesorder'
        request.No = 'ABC-123'

        # Obtain the connection object
        conn = self.soap['Sales Order Read']

        # Invoke the SOAP endpoint
        response = conn.invoke('Read', request)

        # Read the response with dot access
        self.logger.info('Order status: %s', response.ReadResponse.SalesOrder.Status)

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

The operation name - Read here - becomes the element inside the SOAP body, the request message becomes its children, and the parsed response comes back as the same kind of object, so reading it is dot access again.

Using dynamic data

Since requests are ordinary Python objects, dynamic data is ordinary Python assignment - no string formatting and no escaping concerns:

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

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

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

class CreateOrder(Service):

    name = 'example.soap.order.create'

    def handle(self) -> 'None':

        # Input from caller
        order_no = self.request.input['order_no']
        vendor = self.request.input['vendor']
        amount = self.request.input['amount']

        # Build the SOAP request with dynamic data
        request = SOAPMessage()
        request.namespace = 'urn:example-schemas/codeunit/OrderService'
        request.orderNo = order_no
        request.vendor = vendor
        request.amount = amount

        # Invoke the SOAP endpoint
        response = self.soap['Order Create'].invoke('CreateOrder', request)

        # Return the response to caller
        self.response.payload = response.CreateOrderResponse.return_value

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

Embedding multiple items

When a SOAP request needs to contain a list of items, assign a list - each element of the list becomes a repeated XML element:

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

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

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

class CreateSalesOrder(Service):

    name = 'example.soap.sales-order.create'

    def handle(self) -> 'None':

        # Input from caller
        order_no = self.request.input['order_no']
        customer_no = self.request.input['customer_no']
        items = self.request.input['items']

        # Build one node per line item
        lines = []

        for item in items:
            line = SOAPMessage()
            line.No = item['no']
            line.Description = item['description']
            line.Quantity = item['quantity']
            line.Unit_Price = item['unit_price']
            lines.append(line)

        # Build the SOAP request with embedded items
        request = SOAPMessage()
        request.namespace = 'urn:example-schemas/page/salesorder'
        request.SalesOrder.No = order_no
        request.SalesOrder.Customer_No = customer_no
        request.SalesOrder.Lines.Line = lines

        # Invoke the SOAP endpoint
        response = self.soap['Sales Order Create'].invoke('Create', request)

        # Return the new order's number to caller
        self.response.payload = response.CreateResponse.SalesOrder.No

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

Handling SOAP faults

When the endpoint rejects a request, the error surfaces as a SOAPFault exception - you never parse fault XML yourself:

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

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

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

class ReadOrderSafely(Service):

    name = 'example.soap.order.read-safely'

    def handle(self) -> 'None':

        request = SOAPMessage()
        request.No = self.request.input['order_no']

        try:
            response = self.soap['Sales Order Read'].invoke('Read', request)
        except SOAPFault as fault:
            self.logger.warning('SOAP fault: %s %s', fault.code, fault.reason)
            self.response.status_code = 502
        else:
            self.response.payload = response.ReadResponse.SalesOrder.Status

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

Going further

The connection's SOAP and Security tabs unlock the rest of the protocol without any code changes - WS-Security definitions, WS-Addressing headers and MTOM attachments for binary payloads. The SOAP integrations pillar covers all of it.

Continue your API learning journey

Learn more