Its Fake Site ? ⚠️

SOAP API

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)
PayloadJSONXML envelope
ContractOpenAPI schemaWSDL (machine-readable, client autogeneration)
ErrorsHTTP status codes (404, 429…)SOAP Fault → HTTP 500 + <soap:Fault>; transport-level problems keep plain HTTP codes (400/405/413/415) — see Faults below
VerbsGET/POST/PUT/DELETE per resourceSOAP 1.1 over HTTP: POST to one service endpoint, the operation travels inside the SOAP Body

Endpoint

  • Service: POST https://apilearn.tukas.dev/soap/service
  • Contract: GET https://apilearn.tukas.dev/soap/service?wsdl — note ?wsdl is a widespread framework convention for publishing the contract, not part of the SOAP spec itself
  • Content-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 alone
  • Namespace: https://apilearn.tukas.dev/soap/shop

Quick start with zeep

The 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)

Operations

GetCategories

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>'

GetProducts

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)

GetProduct

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.

Authentication — WS-Security UsernameToken

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:

  • Only PasswordText is accepted (over TLS). The alternative, PasswordDigest, is computed as a hash of nonce + created + the password or a password-equivalent secret, so the server has to hold that secret to recompute it. This service stores ordinary one-way password hashes and keeps no such shared secret, so it cannot verify the standard digest form. Send one and you get 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.
  • Auth failures use the fault QNames the WS-Security spec itself defines: 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.
  • Too many failed logins from one address → a temporary lockout fault. Look at where the retry information arrives: in a 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.
  • The Security header is processed whenever present — wrong credentials fault even on public operations.
  • A header this server does not understand, flagged 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.
  • WSDL 1.1 can describe SOAP header message parts, and their errors, via 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.

GetMyOrders

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.

CreateOrder

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.

Faults — errors, the SOAP way

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.

Not everything is a Fault

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 XML400 Bad Request, no envelope
DTD / entity definitions in the payload400 — rejected outright; look up "XXE" to learn why
GET on the service endpoint405 Method Not Allowed
body over 64 KB413 Payload Too Large
Content-Type other than text/xml415 Unsupported Media Type
valid SOAP, application error500 + <soap:Fault>

Catalog operations are anonymous; orders require WS-Security auth. The WSDL is the source of truth for what exists right now.