What a legacy SOAP integration actually gives you

A legacy SOAP integration is usually inherited rather than chosen. Somebody built a web service against a finance or distribution system eight or twelve years ago, it has run without incident since, and now a newer system needs to talk to it. The instinct is to treat the SOAP endpoint as an inconvenience to be hidden behind something more modern. That instinct is broadly correct, but it is worth being precise about what you are hiding, because the endpoint is frequently the last piece of code standing between an order and a posted ledger entry.

The useful property of SOAP is that the contract is explicit. A WSDL document, with its referenced XSD schemas, tells you the operation names, the shape of every request and response, the data types of individual fields, and which elements are optional. Compared with a REST API documented in a PDF that was last revised in 2016, that is a substantial advantage. The schema is machine-readable and can be used to generate a client, which removes an entire category of guesswork from the build.

The unhelpful properties are equally consistent. SOAP services of this vintage tend to be stateful, using a session identifier obtained from a login operation and expiring after a fixed idle period. They often return business failures as SOAP faults over HTTP 500, which confuses generic retry logic. They frequently model numbers as strings, dates without time zones, and optional values as empty elements rather than nillable ones. Microsoft Dynamics NAV, for example, exposes pages and codeunits as SOAP services with a record key that carries concurrency information; ignore it and your update silently overwrites somebody else's. SAP Business One's DI Server works on a session identifier passed in the envelope header, and will refuse work once that session lapses.

None of this is a reason to avoid the endpoint. It is a reason to spend the discovery stage reading the contract properly before anyone writes a line of code, and to treat the finance impact of every failure mode as a design input rather than an afterthought.

Reading the WSDL before you write any code

Generating a client from a WSDL takes about ten minutes. Understanding what the schema permits takes considerably longer, and that gap is where most production incidents originate. The schema describes structure, not business rules. It will happily accept a sales line with a tax code that does not exist in the destination company, a nominal code that has been archived, or a quantity of zero. The service may accept the call, return success, and create a document that finance will spend a day unpicking at month end.

The specific traps we look for during data mapping are consistent enough to list.

The output of this stage should be a field-by-field mapping document that records, for every element the facade will populate, the source field, the transformation, the permitted values, the behaviour when the source value is absent, and who signed it off. If a value cannot be derived from the source system, that is a business decision about a default, not a technical decision for the developer to take quietly.

It is also worth confirming whether the WSDL you have been given matches the endpoint you will be calling. On-premise services are frequently patched without the published contract being regenerated. Retrieve the WSDL from the live endpoint itself, compare it against whatever was emailed to you, and note the differences in writing before the scope is fixed.

  • Decimal handling — Quantities and unit prices declared as xs:string or xs:double rather than xs:decimal. Doubles cannot represent values such as 0.1 exactly, and the error compounds across a long order. Parse to decimal at the facade boundary, carry the scale the destination expects, and never let a floating-point type touch a monetary value.
  • Optional versus nillable — An element with minOccurs="0" may be omitted; an element marked nillable takes xsi:nil="true". Sending an empty string where the service expects the element to be absent often results in the field being overwritten with a blank rather than left unchanged.
  • Dates without offsets — xs:dateTime values with no time zone component are interpreted in the server's local time. For a document dated 31 March at 23:40 UTC, that difference moves the transaction into a different VAT period.
  • Fixed-length fields — Many underlying tables impose character limits that the schema does not declare. A customer reference of thirty characters truncated to twenty by the destination breaks reconciliation later, and does so without raising an error.
  • Array conventions — Check whether repeated elements are wrapped in a container element and whether an empty collection should be sent as an empty wrapper or omitted entirely. Services differ, and the wrong choice often produces a document header with no lines.

Faults, timeouts and the duplicate posting problem

The most expensive failure in a finance-facing legacy SOAP integration is not an error. It is a timeout. The calling system waits thirty seconds, gives up, and retries. The SOAP service, meanwhile, completed the posting on the forty-second second and is quietly waiting to return a response nobody is listening for. The retry creates a second invoice. Nobody notices until the customer queries a statement, or until the VAT return is being prepared and the control account does not agree.

The defence has three parts. First, classify faults properly. A SOAP 1.1 fault with a faultcode in the Client namespace usually indicates a request the service will never accept: a bad tax code, a missing customer, a validation breach. Retrying it is pointless and should be treated as a permanent failure routed to a dead letter store for human review. A Server fault, a socket timeout, a connection reset or an expired session are transient and may be retried with backoff. Blanket retry policies that do not distinguish between the two are the single most common cause of duplicate documents we are asked to clean up.

Second, make the call idempotent from the caller's side, because the legacy service almost certainly will not offer an idempotency header. Nearly every finance system has a field intended for an external reference: the customer reference or a user-defined field on the document header. Write a deterministic key into it, derived from the source document identifier, before any posting attempt. On retry, query the service for a document carrying that key. If one exists, the previous attempt succeeded and the retry should be recorded as a no-op rather than executed. This pattern works equally well against Sage 200 and against a modern REST destination such as Xero, where the invoice number or a reference field serves the same purpose.

Third, accept that a lookup-then-post sequence has a window in which both calls can be in flight. Serialise postings per source document, use a queue with a single consumer per partition key, and set the service call timeout longer than the destination's own transaction timeout so that the caller never gives up before the server has finished. If the underlying database commits in twenty seconds under load, a fifteen-second client timeout is a duplicate generator, not a safety measure.

Designing the REST facade so finance can trust it

The facade exists to give consuming systems a stable, documented, JSON-over-HTTPS interface while the SOAP service continues to do the work. Its value is in the boundaries it enforces, not in the protocol translation, which is the easy part. A well-designed facade validates the payload against the business rules discovered during mapping before it constructs a single envelope. Invalid tax codes, unknown nominal accounts, out-of-range dates and unmatched customer references are rejected at the door with a specific, actionable message, rather than being forwarded to the legacy service to fail in a less informative way.

Error semantics need deliberate design. Map permanent SOAP faults to 4xx responses with a machine-readable error code and the offending field named; map transient conditions to 503 with a Retry-After header. Never return 200 with an error message in the body, and never return a generic 500 that a consuming system will retry indefinitely. Where the facade has accepted work for asynchronous processing, return 202 with a status URL, and make the status resource state clearly whether the document reached the ledger, was rejected, or is still in flight.

Every call should be logged as a correlated pair: the inbound JSON request, the outbound SOAP envelope, the raw response, the fault if any, the derived idempotency key and the resulting document number. Store the envelopes, not a summary of them. When a finance manager asks in six months why a particular credit note carries the wrong nominal code, the answer needs to be retrievable in minutes. Agree the retention period during scoping, and be mindful of any personal data inside the envelopes when setting it.

Finally, version the facade from day one. If the legacy WSDL changes because someone applies a service pack, the facade absorbs the change and its own contract stays stable. That is the entire point of the exercise. Pin the generated client to a known WSDL revision, checksum the live WSDL on a schedule, and raise an alert when it moves.

Transport, security and the operational details that bite

Older SOAP endpoints are often reachable only from inside a network, secured with WS-Security UsernameToken over TLS, and served by an application server with modest concurrency limits. Each of these becomes an operational constraint on the integration. If the facade runs in a cloud environment, you will need a site-to-site VPN or a permitted egress address, and somebody in IT must own that arrangement. If the service supports only TLS 1.0 or 1.1, modern client libraries will refuse to negotiate, and the choice is between upgrading the host and accepting a documented, isolated exception on a locked-down network segment.

Session management deserves explicit attention. A service that issues a session token on login and expires it after twenty minutes of inactivity will fail intermittently under a bursty workload, typically at nine in the morning after a quiet night. Hold a small pool of sessions, refresh them on a timer rather than on failure, and treat a session-expired fault as transient with an automatic re-login and a single retry. Where the underlying system licences by concurrent connection, as several ERP products do, cap the pool below the licence count and leave headroom for interactive users, who will otherwise be locked out by the integration.

Certificate expiry is the most avoidable outage in this category of work. Client certificates, server certificates and the trust chain all need monitored expiry dates with alerts at sixty and thirty days. Put the renewal owner in the handover documentation by role, not by individual name. The same applies to the service account password, which in older estates is often set never to expire and therefore falls outside anyone's routine.

Throughput is worth measuring rather than assuming. Time a single posting against a copy of the production data at a realistic document size, then decide whether the required daily volume fits inside the available window. If a five-hundred-line consolidated invoice takes four minutes, batching becomes a design question rather than an optimisation.

Acceptance testing and handover for a finance-facing bridge

Acceptance testing for this kind of work is not a developer activity. It is a finance activity supported by a developer. The test set should be drawn from real documents, not invented ones: a standard domestic sale, a zero-rated sale, a sale to a customer on credit hold, a multi-line order with mixed tax codes, a part-shipment, a credit note against a prior invoice, and a document dated on the boundary of a VAT period. For each, the expected ledger outcome is written down before the test runs, and the actual outcome is checked in the destination system by the person who owns the ledger.

Failure paths need the same treatment. Deliberately submit a document with an unknown customer reference and confirm it is rejected with a clear message and no partial posting. Stop the SOAP service mid-run and confirm the queue holds the work and resumes without duplication. Force a timeout and confirm the idempotency check prevents a second document. These tests are tedious, and they are the ones that determine whether the first month-end after go-live is uneventful.

Where the volume justifies it, run in parallel for one full period. The legacy process continues as normal while the new bridge posts into a copy company or a clearly marked test company, and the two are compared on totals by nominal code, by tax code and by document count. A reconciliation report that shows agreement for a complete month is the most persuasive evidence available that the bridge is safe to make live, and it usually surfaces one or two mapping decisions that nobody thought to ask about during discovery.

Handover should include the WSDL revision the client was generated against, the field mapping document with its sign-offs, the error code reference, the runbook for session and certificate renewal, and the source code. If the arrangement ends, the organisation should be able to hand the whole package to another engineer and have them productive in a day. That is the standard we work to, and it is the reason scope is fixed in writing before any code is produced. If you are weighing up a project of this shape, we are on 01303 883111 or at hello@api-integrations.co.uk.

Key points

  • Classify SOAP faults into permanent and transient before writing any retry logic; blanket retries against a finance endpoint are the usual cause of duplicate invoices.
  • Write a deterministic idempotency key into an external reference field on the document header, and query for it before every retry, because legacy services will not offer one.
  • Read the WSDL and XSD for decimal types, nillable elements and time-zone-free dates during discovery, and record every field mapping decision in writing before the scope is fixed.