Using Zeep to Make Soap Requests in Python

While most APIs are REST these days, there are still some SOAP ones out there — typically enterprise software or if you need the benefits of WS-security or the like. What brought me to SOAP was integrating with an enterprise payment gateway that had developed their own clients in several languages, but not Python, so I was on the hunt for my own. There are others, the short version of why I went with Zeep is because it was purported to be the most “modern” — we can argue about whether this was a good decision or rationale some other time. This is a pretty basic overview, but should enable you to make your first requests and clear up some hurdles I faced when some things were less clear than I would have liked in the docs.

One of the nice things about using Zeep is that you don’t actually need to write XML, which can be finicky. Create a dictionary with the relevant data, and it will create XML for you, based on an XML schema that you provide it (more on that later). For example, the following dictionary:

request_data = {
    'merchantID': '1234abc',
    'billTo': {
        'street1': '400 Broad Street',
        'city': 'Seattle',
        'state': 'WA',
        'postalCode': '98109',
        'country': 'US',
    },
    'item': [{
        'id': '0',
        'unitPrice': '12.34',
        'quantity': '1',
    }],
    'taxService': {
        'run': "true",
        'nexus': "WA, CA" 
    },
}

Results in the following XML:

<merchantID>1234abc</merchantID>
<billTo>
  <street1>400 Broad Street</street1>
  <city>Seattle</city>
  <state>WA</state>
  <postalCode>98109</postalCode>
  <country>US</country>
</billTo>
<item id="0">
  <unitPrice>12.34</unitPrice>
  <quantity>1</unitPrice>
</item>
<taxService run="true">
  <nexus>WA, CA</nexus>
</taxService>

It’s worth noting that parameters are passed into Zeep the same way fields are. The schema includes both attributes and fields, so they are applied correctly. Another thing to note is that boolean values should be passed in as lowercase strings, not as Python boolean values — Zeep doesn’t know to translate from an actual boolean.

Update: heard from a reader looking to include both an attribute and a field in the same element — i.e if I had wanted the tax_service element above to read WA, CA instead. That can be accomplished by doing something like

'taxService': {
        'run': "true",
        '_value_1': "WA, CA" 
    },

Find it mentioned here in the docs.

Zeep gets access to the XML schema mentioned above via the WSDL that is passed into the Zeep client when it is initialized. A WSDL is a Web Service Description Language — it describes what an endpoint can do; what parameters and operations it accepts, and also links to an XML schema. The WSDL typically contains something like this:

<wsdl:types>
  <xsd:schema>
    <xsd:import namespace="<namespace>" schemaLocation="<schema>"/>
  </xsd:schema>
</wsdl:types>

Or:

<definitions name="<name>"
   targetNamespace="<service>"
   xmlns:xsd="<schema>">
...
</definitions>

In both cases, <schema> will point to another location on the web where a detailed schema can be found.

The client can be initialized like so:

from zeep import Client
from zeep.wsse.username import UsernameToken

client = Client(<wsdl_url>, wsse=UsernameToken(<username>, <password>)

This gives the client both the WSDL and your security credentials via WS-Security. It will generate an appropriate XML envelope.

To use the client, you’ll need to know which methods are available to it. This is not native to Zeep, which makes it a bit harder to parse if your habit for figuring out how things work, like mine, is going into the source code itself — you won’t find the method names there. You’ll need to see what operations are available to the API you’re using. The WSDL that I created my client with includes this line:

<wsdl:operation name="runTransaction">

This means I have access to runTransaction as a method on my client service, which I can use like so:

client.service.runTransaction(**request_data)

Zeep handles XML parsing the response for you too. The response object you’ll get back can be accessed just like a dictionary, but isn’t a dictionary. This means you can do things like response['key'], but you can’t do response.get('key', None). The good news is that if a given key doesn’t exist, it won’t throw a KeyError, but will just return None.