A real SOAP 1.1 web service over the same fake furniture shop. SOAP still runs banking, government, and logistics integrations — reading a WSDL, composing an envelope, and debugging a Fault are skills that set you apart. Public SOAP practice targets are nearly extinct; this one is yours.
| REST (this site's API) | SOAP (this page) | |
|---|---|---|
| Payload | JSON | XML envelope |
| Contract | OpenAPI schema | WSDL (machine-readable, client autogeneration) |
| Errors | HTTP status codes (404, 429…) | SOAP Fault → HTTP 500 + <soap:Fault>; transport-level problems keep plain HTTP codes (400/405/413/415) — see Faults below |
| Verbs | GET/POST/PUT/DELETE per resource | SOAP 1.1 over HTTP: POST to one service endpoint, the operation travels inside the SOAP Body |
POST https://apilearn.tukas.dev/soap/serviceGET https://apilearn.tukas.dev/soap/service?wsdl — note ?wsdl is a
widespread framework convention for publishing the contract, not part of the SOAP
spec itselfContent-Type: text/xml; charset=utf-8 (anything else → HTTP 415)SOAPAction: required by this SOAP 1.1 service and must match the
operation declared in the WSDL; wrong or missing → Fault. The operation itself is
identified by the element inside the Body — WS-I calls SOAPAction a
hint that a SOAP receiver must not rely on alonehttps://apilearn.tukas.dev/soap/shopThe fastest way in: let the WSDL do the work.
pip install zeep
# dump the contract — operations, signatures and types:
python -m zeep https://apilearn.tukas.dev/soap/service?wsdl
from zeep import Client
client = Client("https://apilearn.tukas.dev/soap/service?wsdl")
for c in client.service.GetCategories():
print(c.id, c.name, c.slug)
page = client.service.GetProducts(page=1, pageSize=5, onSale=True)
print(page.totalCount, "products on sale")
for p in page.product:
print(p.name, p.sellPrice)
SOAPAction: "https://apilearn.tukas.dev/soap/shop/GetCategories" — no parameters.
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<shop:GetCategories xmlns:shop="https://apilearn.tukas.dev/soap/shop"/>
</soap:Body>
</soap:Envelope>
curl:
curl -s https://apilearn.tukas.dev/soap/service \
-H 'Content-Type: text/xml; charset=utf-8' \
-H 'SOAPAction: "https://apilearn.tukas.dev/soap/shop/GetCategories"' \
--data '<?xml version="1.0"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><shop:GetCategories xmlns:shop="https://apilearn.tukas.dev/soap/shop"/></soap:Body></soap:Envelope>'
SOAPAction: "https://apilearn.tukas.dev/soap/shop/GetProducts" —
all parameters optional: page (default 1), pageSize
(default 20, max 100), categorySlug, onSale,
q (full-text search, same engine as the site search).
The 1..100 bound on pageSize is not documentation prose — it is an
xs:restriction in the WSDL's schema, machine-readable by any tool that
reads the contract. The distinction worth taking away is not SOAP versus REST —
OpenAPI's schemas are JSON Schema and can express comparable bounds — but
machine-readable schema versus prose-only documentation. Whichever
stack you are on, ask which of the two a stated limit actually is.
A useful reality check while you are here: a contract constraint and your
client library's validation are not the same thing. zeep enforces
structure — ask CreateOrder for 21 item elements
and it raises a ValidationError locally, because
maxOccurs="20" is cardinality. It does not enforce simple-type
facets: pageSize=5000, a malformed phoneNumber or a
too-short deliveryAddress are all serialised and sent, and it is this
server that answers with a Fault. Other stacks (.NET, Java JAX-WS, a validating XML
parser) may check facets, depending on the stack and how it is configured. Never
assume the client is enforcing the contract for you —
the constraint is real, but the enforcement point may not be where you expect.
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<shop:GetProducts xmlns:shop="https://apilearn.tukas.dev/soap/shop">
<shop:page>1</shop:page>
<shop:pageSize>5</shop:pageSize>
<shop:onSale>true</shop:onSale>
</shop:GetProducts>
</soap:Body>
</soap:Envelope>
The response carries page, pageSize, totalCount,
totalPages and repeated product elements. Compare the field
names with GET /api/products/ — same data, camelCase instead of
snake_case: that mapping exercise is intentional.
Python zeep:
found = client.service.GetProducts(q="table", pageSize=10)
for p in found.product:
print(p.slug, p.price, "→", p.sellPrice)
SOAPAction: "https://apilearn.tukas.dev/soap/shop/GetProduct" —
requires slug.
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<shop:GetProduct xmlns:shop="https://apilearn.tukas.dev/soap/shop">
<shop:slug>tea-table-set-three-chairs</shop:slug>
</shop:GetProduct>
</soap:Body>
</soap:Envelope>
One product comes back, or a ProductNotFound fault. The
Product type (shared with GetProducts) carries
id, name, slug, description,
price, discount, sellPrice,
quantity, a nested category (id,
name, slug), imageUrl and
productUrl. description and imageUrl are
minOccurs="0" — absent, not empty, when the product has none.
GetMyOrders and CreateOrder require credentials — the same
account you use on the site and the REST API. SOAP can ride on whatever the
transport offers, but this service does not authenticate with HTTP
headers or cookies: credentials travel inside the envelope, in a
wsse:Security SOAP Header defined by the WS-Security standard:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
soap:mustUnderstand="1">
<wsse:UsernameToken>
<wsse:Username>your-username</wsse:Username>
<wsse:Password>your-password</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</soap:Header>
<soap:Body>
<shop:GetMyOrders xmlns:shop="https://apilearn.tukas.dev/soap/shop"/>
</soap:Body>
</soap:Envelope>
In zeep it's one argument:
from zeep import Client
from zeep.wsse.username import UsernameToken
client = Client("https://apilearn.tukas.dev/soap/service?wsdl",
wsse=UsernameToken("your-username", "your-password"))
for order in client.service.GetMyOrders():
print(order.id, order.status, order.createdTimestamp)
Worth knowing:
wsse:UnsupportedSecurityToken saying so — note the code:
WS-Security reserves wsse:UnsupportedAlgorithm for signature and
encryption algorithms, while an unsupported token form is this one. A
very common real-world integration surprise.wsse:FailedAuthentication (bad credentials),
wsse:InvalidSecurity (missing or broken Security header),
wsse:InvalidSecurityToken, wsse:UnsupportedSecurityToken.
faultcode is a QName and the set is extensible, so this is fully
legitimate — the contrast to draw is with Body errors below, which stick
to the four standard SOAP codes.shop:AuthRateLimit element in the
response's SOAP Header, not in <detail>.
That is not a stylistic choice — SOAP 1.1 reserves <detail> for
errors about the Body and requires header-related error information to
travel in a header entry. (WSDL 1.1 has soap:headerfault for
describing exactly this; wiring it here would mean importing the OASIS WS-Security
schema to reference wsse:Security as a header part, so the contract
declares the AuthRateLimit element's shape directly instead.)
Notice also why this throttle lives in the application at all: every SOAP
operation shares one URL, so URL-based rate limiting (which guards
/user/login and /api/auth/ here) cannot see
authentication happening inside envelopes.mustUnderstand="1", gets a soap:MustUnderstand fault —
the fourth standard faultcode, try it. Add
soap:actor="urn:anything" to that same header and the fault
disappears: the actor attribute addresses a block to a specific
node, and a header meant for an intermediary is none of the final receiver's
business — flag included. Omitting actor targets the ultimate
receiver; the special value
http://schemas.xmlsoap.org/soap/actor/next targets every node on
the path. The same rule governs wsse:Security: credentials
addressed to a gateway are not consumed here.soap:header and soap:headerfault. What it cannot
express on its own is the security policy: that a UsernameToken is
required here, which password form is accepted, what transport protection is
assumed. Those are the job of WS-Policy / WS-SecurityPolicy — which is why this
section exists in prose.SOAPAction: "https://apilearn.tukas.dev/soap/shop/GetMyOrders" —
no parameters, auth required. Returns your orders: id,
createdTimestamp (xs:dateTime), phoneNumber,
requiresDelivery, deliveryAddress (omitted when empty),
paymentOnGet, isPaid, status, and repeated
item lines of name, price,
quantity. The order fields are GET /api/orders/'s in
camelCase; the item lines drop REST's id, which identifies nothing you
can address here.
SOAPAction: "https://apilearn.tukas.dev/soap/shop/CreateOrder" —
auth required. Unlike the REST flow there is no cart: order lines travel in the
request as repeated item elements (the contract's first nested list
input — check the WSDL's OrderItemInput type):
<shop:CreateOrder xmlns:shop="https://apilearn.tukas.dev/soap/shop">
<shop:item>
<shop:productSlug>tea-table-set-three-chairs</shop:productSlug>
<shop:quantity>2</shop:quantity>
</shop:item>
<shop:phoneNumber>+1-555-0100</shop:phoneNumber>
<shop:requiresDelivery>false</shop:requiresDelivery>
<shop:paymentOnGet>true</shop:paymentOnGet>
</shop:CreateOrder>
order = client.service.CreateOrder(
item=[{"productSlug": "tea-table-set-three-chairs", "quantity": 2}],
phoneNumber="+1-555-0100",
paymentOnGet=True,
)
print(order.id, order.status)
Rules mirror the REST API, and most of them are in the schema rather than in this
paragraph — item is maxOccurs="20", quantity
is tns:Quantity (1-100), phoneNumber is
tns:PhoneNumber (a character pattern), deliveryAddress is
tns:DeliveryAddress (50-500 characters; omit the element entirely when
you don't need delivery, and it is required when requiresDelivery is
true). Two rules cannot live in XSD and are enforced only by the service, both
faulting with InvalidParameter: the phone must contain 7-15 actual
digits (an XSD pattern has no lookahead to count them), and duplicate
productSlug values in one request are rejected instead of merged.
Beyond the schema: 50 test orders per account per day, and stock is checked but
never deducted. Submitted phone/address are validated, then replaced with
surrogates before storage — same privacy model as REST; what comes back is
the placeholder, not what you sent, which is why the returned Order
keeps plain xs:string for those two fields. Declared faults (see the
WSDL): ProductNotFound, InsufficientStock (with requested
vs available), QuotaExceeded (with retryAfterSeconds —
compare with REST's 429 + Retry-After header: same rule, different
protocol surface), InvalidParameter, MissingParameter.
Ask for a product that does not exist and you get HTTP 500 — not 404. That is not a bug: the SOAP 1.1 spec says a response carrying a Fault must use status 500, and the actual error contract lives inside the body:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>soap:Client</faultcode>
<faultstring>No product with slug 'slug-not-exists'.</faultstring>
<detail>
<shop:ProductNotFound xmlns:shop="https://apilearn.tukas.dev/soap/shop">
<shop:slug>slug-not-exists</shop:slug>
</shop:ProductNotFound>
</detail>
</soap:Fault>
</soap:Body>
</soap:Envelope>
Faults raised while processing the Body — everything in this section —
use the four standard SOAP 1.1 fault codes:
Client ("your request is wrong, don't retry it unchanged"),
Server ("our side broke, retry may help"),
VersionMismatch (wrong Envelope namespace — send a SOAP 1.2 envelope here
to see it), MustUnderstand. faultcode is a QName and the set
is extensible, so custom codes from a namespace you control are legitimate — that is
exactly what the wsse:* codes above are — but WS-I recommends what you see
here for application errors: standard codes, with the specific error in
machine-readable form inside <detail> (SOAP 1.1 requires
detail whenever the Body could not be processed), and no dotted extensions
like Client.ProductNotFound.
The six detail elements — ProductNotFound, InvalidPage,
InvalidParameter, MissingParameter,
QuotaExceeded, InsufficientStock — are declared in the WSDL's
schema and attached via wsdl:fault to the operations that can
raise them, so you can see which operation fails in which way before ever calling it.
The one thing you will not find in any <detail> is header
error information: AuthRateLimit arrives in the response's SOAP Header
instead, because SOAP 1.1 scopes <detail> to Body errors. In zeep
every Fault surfaces as zeep.exceptions.Fault with .code,
.message and .detail.
Faults are for well-formed SOAP requests that fail. Problems below the SOAP layer get plain HTTP status codes, per WS-I Basic Profile — practice triggering each:
| malformed XML | 400 Bad Request, no envelope |
| DTD / entity definitions in the payload | 400 — rejected outright; look up "XXE" to learn why |
GET on the service endpoint | 405 Method Not Allowed |
| body over 64 KB | 413 Payload Too Large |
Content-Type other than text/xml | 415 Unsupported Media Type |
| valid SOAP, application error | 500 + <soap:Fault> |
Catalog operations are anonymous; orders require WS-Security auth. The WSDL is the source of truth for what exists right now.