Common Login Portal
Hand-rolling SOAP clients against undocumented APIs
Ashish Bhagat · 13 Aug 2026
CLP verifies two things about every organization that registers: its labour and work-permit status against the Ministry of Labour, and its commercial registration against the Commercial Registry. Both are SOAP APIs. Neither has a WSDL you can actually build a client from.
What a WSDL is supposed to buy you
Both integrations exist for the same reason: CLP won't register an organization without verifying it against government records first. Labour and work-permit status against MOL, commercial registration against MOCI. That verification step is a hard gate in the registration flow, not a cosmetic check — it's the thing that decides whether an organization is who it claims to be before it gets access to anything downstream. Whatever technical debt exists on the client side, it sits directly in front of the one part of registration that's actually load-bearing for trust in the whole system.
The normal path for a SOAP integration is to point a code generator — wsimport and equivalents exist across most JVM SOAP tooling — at the service's WSDL, and get back typed request and response classes for free. You call methods on a generated stub, the marshaling and unmarshaling between XML and Java objects happens automatically, and the compiler catches it if you send a field of the wrong type or misspell an element name. It's the entire point of publishing a WSDL in the first place: it lets consumers stop hand-writing XML.
That path requires a WSDL that's actually usable — reachable, internally consistent, and describing a contract that matches what the live service actually does. Government SOAP endpoints don't reliably provide that. When the WSDL a service publishes doesn't hold up well enough to generate a working client from, stub generation isn't a shortcut anymore, it's a dead end, and the only way forward is to stop trusting the WSDL to describe the contract and start treating the raw request and response XML as the actual source of truth.
DOM and XPath instead
This isn't an unusual position to end up in when the counterparty is a government system rather than a modern SaaS API. Plenty of well-established government and enterprise SOAP services predate widespread WSDL tooling discipline, or were built by teams optimizing for "the endpoint answers correctly" rather than "the endpoint is a pleasant, generator-friendly contract for outside consumers." Nobody involved was wrong to build it that way at the time. It's just not the shape a modern client stack is built to consume automatically.
Both MOL and MOCI integrations are built as hand-rolled SOAP clients: construct the request envelope as XML directly, send it over a plain HTTP client, and parse whatever comes back using a DOM parser and XPath expressions targeting the specific elements the response actually needs to yield — no generated bindings, no compile-time contract, just XML in and XML out, inspected explicitly.
Reconstructed for this post, not the literal production code, but the actual shape of it — build the envelope, post it, walk the response with XPath:
// Illustrative — shows the approach, not the production implementation
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document response = builder.parse(soapResponseStream);
XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setNamespaceContext(molNamespaceContext);
String workPermitStatus = xpath.evaluate(
"//*[local-name()='WorkPermitStatus']/text()", response);
Namespaces are the part of this that trips people up first. A real SOAP response from a government service almost never comes back with clean, unprefixed element names — it's wrapped in one or more XML namespaces, often with prefixes that don't match anything you'd guess from the WSDL, if the WSDL is even trustworthy enough to guess from in the first place. An XPath expression written against unprefixed element names will silently match nothing against a namespaced document, which is its own quiet failure mode: no exception, just an empty result, indistinguishable at first glance from "the field legitimately wasn't in the response." The two ways around that are registering the actual namespace context so your expressions can reference prefixes correctly, or sidestepping the problem with local-name() — matching on the element's name regardless of what namespace it's wrapped in, which is what the illustrative expression above does. It's a slightly blunt tool, but it's a reasonable trade when you don't fully trust the namespace declarations to stay consistent across environments either.
Every field this touches is a string you're trusting to be there, in the shape you expect, because nothing upstream of your own code checked that for you. A generated stub would have failed to compile, or failed to deserialize loudly, the moment a response didn't match the contract. A DOM-and-XPath client just returns whatever's actually at that path — or an empty result if it isn't — and moves on.
What a WSDL actually was supposed to fail loudly on
The specific failure this exposes is worth naming precisely, because it's easy to conflate "no compile-time type safety" with "no safety at all," and those aren't the same thing. Generated stubs from a trustworthy WSDL give you two separate guarantees: that the shape of what you send matches the contract, and that the shape of what you get back does too — a response that doesn't match the expected schema fails to deserialize, loudly, at the point of failure. Hand-rolled DOM and XPath parsing keeps the first guarantee if you're careful about how you build the outbound envelope, but gives up the second one entirely. An XPath expression pointed at the wrong element, or an assumption about a field's possible values that turns out to be wrong, doesn't fail to deserialize. It just returns something — an empty string, the wrong string — and your code has to decide for itself whether that's a real answer or a silent miss.
What that costs you
That's not a hypothetical risk. A separate investigation into intermittent 500s and 503s against the same MOL integration turned up exactly this failure mode: the production API returns the string "WORKING" for an active work permit, not "Active". Whatever assumption had been made about that field's value was wrong, and because there was no generated type checking that assumption against a schema, the mismatch didn't throw an exception or fail a build — it just silently evaluated to a mismatch every time, blocking real users from registering with no error anywhere pointing at the cause. That's the direct cost of giving up generated bindings: you get to talk to an endpoint whose WSDL was never going to give you a working client, and in exchange you're personally responsible for every assumption a stub generator would normally have verified for you.
What I'd do differently
Without a trustworthy WSDL, the only real substitute for compiler-enforced contract checking is a set of tests built against actual captured responses from the live service, not assumed values written from documentation or memory. If I were building this again, I'd capture real request/response pairs from both government services early, during initial integration, and turn them into fixture-based tests for every field the client extracts — so a mismatch like "WORKING" versus "Active" gets caught by a failing test against a real recorded response, instead of by a root-cause investigation into an unrelated set of intermittent errors months later.
More from this series:
It was never the network · Mock and real adapters behind every port