Overview

The SOAP services that core banking systems expose represent decades of proven business logic. The WSDL contract documents every operation, every input type, and every fault condition — an asset that most modern REST API implementations do not have. What SOAP lacks, from the perspective of a new digital channel or an Open Banking integration, is: JSON payloads, stateless HTTP verbs, predictable status codes, OAuth2 bearer tokens, and a developer portal that does not require understanding WSDL namespace resolution to start building.

The decision is not whether to modernise — it is how. Wrapping places a REST facade in front of the existing SOAP service. The SOAP service continues to run unchanged; new consumers see only REST. Rewriting replaces the SOAP service with a new implementation. The cost difference is typically 10× in engineering time and 30× in risk, because a working SOAP service carries implicit tests in every production transaction it has processed. Wrapping is almost always the right first move.

Two wrapping mechanisms are relevant in the IBM stack: ACE ESQL wrapping (full control, handles any SOAP complexity) and IBM API Connect assembly policies (lower effort, appropriate for simpler operations). Both are described here.

Wrap, don’t rewrite

A working SOAP service is an asset. The wrapper is the modernisation, not the destination. Design the REST facade to be independent of the SOAP backend’s internal model — the REST API should look like a native REST service, not like SOAP with JSON painted on top. When the SOAP backend is eventually retired, the REST API should not need to change. The consumers must not know that SOAP ever existed behind it.

WSDL Analysis

Before writing a line of wrapper code, map the SOAP operations to REST verbs. The mapping is not purely mechanical — SOAP is operation-centric (everything is a POST), REST is resource-centric — but there is a reliable heuristic:

  • Get* / Retrieve* / Query* / Find* operations → GET. The request parameters become query string parameters or path segments. The SOAP input element body becomes the query response body.
  • Create* / Add* / Submit* / Initiate* operations → POST. The SOAP input element body becomes the JSON request body. The created resource URL goes in the Location response header.
  • Update* / Modify* / Change* operations → PUT or PATCH. PUT for full replacement, PATCH for partial update. The SOAP partial-update pattern (where the SOAP request specifies only the fields to change) maps to PATCH.
  • Cancel* / Delete* / Close* operations → DELETE. If the SOAP operation returns data (e.g. a cancellation reference), map it to a 200 with body, not 204 with no content.

For each operation, map the SOAP fault codes to HTTP status codes (detailed in the fault mapping section). Map SOAP complexType definitions to JSON Schema objects — most type mappings are mechanical, with the common exceptions being SOAP arrays (where maxOccurs="unbounded" must be explicitly handled as a JSON array, not as a repeated XML element) and any union or substitutionGroup types that have no clean JSON equivalent.

ACE Wrapping Approach

The ACE wrapping pattern is a three-node message flow: HTTP Input receives the REST request, ESQL Compute builds the SOAP envelope and sets the SOAPAction header, SOAPRequest (or HTTPRequest) sends the envelope to the legacy backend, another ESQL Compute strips the envelope and converts the response body to JSON, and HTTP Reply sends the REST response.

InitiatePaymentSOAPEnvelope.esqlsql
CREATE COMPUTE MODULE BuildSOAPEnvelope

  CREATE FUNCTION Main() RETURNS BOOLEAN
  BEGIN

    -- Propagate the REST JSON body to SOAP XML
    DECLARE soapNS  NAMESPACE 'http://schemas.xmlsoap.org/soap/envelope/';
    DECLARE payNS   NAMESPACE 'http://saib.internal/payments/v2';
    DECLARE secNS   NAMESPACE 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd';

    -- HTTP SOAPAction header (required for SOAP 1.1 routing)
    SET OutputRoot.HTTPRequestHeader."SOAPAction" =
        '"http://saib.internal/payments/v2/InitiatePayment"';
    SET OutputRoot.HTTPRequestHeader."Content-Type" = 'text/xml;charset=UTF-8';

    -- WS-Security UsernameToken from ACE credentials store
    -- (token injected here; OAuth2 Bearer was already validated by API gateway)
    DECLARE svcUser CHARACTER Environment.Variables.SVC_USER;
    DECLARE svcPass CHARACTER Environment.Variables.SVC_PASS;

    -- Build SOAP envelope
    CREATE LASTCHILD OF OutputRoot.XMLNSC NAMESPACE soapNS NAME 'Envelope';
    DECLARE env REFERENCE TO OutputRoot.XMLNSC.(soapNS)Envelope;

    -- Header with WS-Security
    CREATE LASTCHILD OF env NAMESPACE soapNS NAME 'Header';
    DECLARE hdr REFERENCE TO env.(soapNS)Header;
    CREATE LASTCHILD OF hdr NAMESPACE secNS NAME 'Security';
    DECLARE sec REFERENCE TO hdr.(secNS)Security;
    CREATE LASTCHILD OF sec NAMESPACE secNS NAME 'UsernameToken';
    SET sec.(secNS)UsernameToken.(secNS)Username = svcUser;
    SET sec.(secNS)UsernameToken.(secNS)Password = svcPass;

    -- Body with payment initiation request
    CREATE LASTCHILD OF env NAMESPACE soapNS NAME 'Body';
    DECLARE body REFERENCE TO env.(soapNS)Body;
    CREATE LASTCHILD OF body NAMESPACE payNS NAME 'InitiatePaymentRequest';
    DECLARE req REFERENCE TO body.(payNS)InitiatePaymentRequest;

    -- Map REST JSON fields to SOAP XML elements
    SET req.(payNS)DebtorIBAN    = InputRoot.JSON.Data.debtorIban;
    SET req.(payNS)CreditorIBAN   = InputRoot.JSON.Data.creditorIban;
    SET req.(payNS)Amount         = InputRoot.JSON.Data.amount;
    SET req.(payNS)Currency       = InputRoot.JSON.Data.currency;
    SET req.(payNS)RemittanceInfo = InputRoot.JSON.Data.remittanceInformation;
    SET req.(payNS)EndToEndId     = InputRoot.JSON.Data.endToEndId;

    RETURN TRUE;
  END;

END MODULE;

The response stripping compute module does the reverse: it navigates the SOAP body XML, extracts the relevant elements, and writes them to OutputRoot.JSON.Data. The SOAP envelope, WS-Security header, and namespace prefixes never appear in the REST response. Use ESQL FOR loops (not the generic xml-to-json policy) when the SOAP response contains repeated elements — only a FOR loop guarantees correct array construction when maxOccurs="unbounded" might return one element (which the policy treats as an object, not a one-element array).

IBM API Connect Facade

IBM API Connect 10.0.7’s assembly policy approach is appropriate when the SOAP service has straightforward request/response types (no complex SOAP arrays, no custom WS-Security implementations beyond UsernameToken, no SOAP 1.2 faults with custom namespace elements). The APIC developer portal handles OpenAPI documentation, rate plans, and subscription management — this is the primary advantage over the pure ACE approach.

The assembly policy pipeline for a SOAP wrap in APIC: xml-to-json on the SOAP response (after calling the backend), a map policy to rename and restructure the JSON, and a gatewayscript policy to handle edge cases (fault detection, array normalisation, RFC 7807 error shaping). The APIC gateway adds OAuth2 token validation and rate limiting upstream of the assembly without any additional configuration.

Register the SOAP service as an invoke target in the APIC assembly with basic authentication (the SOAP backend’s internal credentials). The APIC gateway holds those credentials in a TLS client profile or a vault secret — they are never exposed to the REST client. The REST client sees only the OAuth2-protected OpenAPI endpoint.

Auth Translation

SOAP services at Saudi banks most commonly use one of three authentication mechanisms: WS-Security UsernameToken (plaintext or password-digest), BasicAuth over TLS, or client certificate (mTLS). REST APIs on consumer-facing channels must support OAuth2 Bearer tokens, and those on Open Banking channels must specifically support FAPI 2.0 (Financial-grade API, Security Profile 2.0) — a requirement under SAMA’s Open Banking Framework.

The translation is not one-to-one. The REST consumer presents a JWT Bearer token. The wrapper validates the token (signature, expiry, scope) at the API gateway layer. The wrapper then constructs the SOAP authentication header using service account credentials it holds internally — the caller’s identity is not forwarded to the SOAP backend. This is the correct pattern: the backend authenticates the integration layer (trusted network, service credentials), and the integration layer authenticates the caller (JWT, OAuth2).

For FAPI 2.0 compliance on the REST facade: the API gateway must validate the dpop proof-of-possession header, enforce PKCE in the OAuth2 authorisation code flow, and include the required FAPI response headers (x-fapi-interaction-id, x-fapi-auth-date) in every response. APIC 10.0.7 supports all of these via its built-in OAuth2 provider; configure the FAPI profile in the API security definition, not in custom gateway scripts.

Never expose SOAP WS-Security credentials through a REST facade

A REST facade that passes the client’s credentials directly into a SOAP WS-Security UsernameToken header — because it is “simpler” than managing service accounts — exposes those credentials in plaintext in the SOAP envelope on the internal network, makes every REST client a SOAP-authenticated principal (bypassing any RBAC on the SOAP layer), and is trivially exploitable by any service on the internal network that can intercept the SOAP call. Translate credentials at the ACE or APIC layer using internally managed service accounts. The REST client must never touch SOAP security mechanics.

SOAP Fault to HTTP Error Mapping

SOAP 1.1 sends all errors as HTTP 200 with a Fault element in the body. This breaks every REST client that relies on HTTP status codes for error detection. The wrapper must inspect the SOAP body and convert faults to appropriate HTTP status codes before returning to the REST client. The ESQL pattern:

FaultMapping.esql + apic-assembly.yamlyaml
# IBM API Connect assembly policy fragment
# Maps SOAP fault codes to RFC 7807 Problem Details JSON
assembly:
  execute:
    - invoke:
        target-url: "$(target-url)"
        verb: POST
        output: soapResponse

    # Step 1: detect SOAP fault before xml-to-json destroys fault structure
    - gatewayscript:
        source: |
          var body = context.get('soapResponse.body');
          var xml  = XML.parse(body);
          var fault = xml.querySelector('Fault');
          if (fault) {
            var faultcode = fault.querySelector('faultcode').textContent;
            var faultstr  = fault.querySelector('faultstring').textContent;
            // SOAP:Client faults → 4xx; SOAP:Server faults → 500
            var httpStatus = faultcode.startsWith('SOAP:Client') || faultcode.startsWith('Client')
              ? 400 : 500;
            // Refine client faults: authentication = 401, authorisation = 403
            if (faultcode.includes('AuthenticationFailed'))  httpStatus = 401;
            if (faultcode.includes('AccessDenied'))          httpStatus = 403;
            if (faultcode.includes('RecordNotFound'))         httpStatus = 404;
            // Emit RFC 7807 Problem Details
            context.set('message.status.code', httpStatus);
            context.set('message.headers.content-type', 'application/problem+json');
            context.set('message.body', JSON.stringify({
              type:   'https://www.wbadawi.info/errors/soap-fault',
              title:  faultstr,
              status: httpStatus,
              detail: faultcode,
              instance: context.get('request.headers.x-fapi-interaction-id')
            }));
            context.reject('SOAPFaultMapped');
          }

    # Step 2: happy path xml-to-json
    - xml-to-json:
        input: soapResponse.body
        output: message.body

    # Step 3: map policy to rename/restructure
    - map:
        inputs:
          body: { schema: "soap-response-schema" }
        outputs:
          body: { schema: "payment-initiation-response-v1" }
        actions:
          - { set: "body.paymentId",       from: "body.InitiatePaymentResponse.PaymentRefNbr" }
          - { set: "body.status",          from: "body.InitiatePaymentResponse.StatusCode" }
          - { set: "body.transactionDate",  from: "body.InitiatePaymentResponse.ValueDate" }

Contract-First Approach

Do not derive the REST API design from the WSDL structure. Write the OpenAPI 3.1 specification first, without looking at the WSDL, as if the SOAP service did not exist. What would a developer building a new payment integration want the API to look like? That is the contract to publish. The WSDL tells you what the backend can do; the OpenAPI spec tells you what the REST client will see. These are different documents with different audiences.

Writing the spec first has a practical benefit: you can generate a mock server from the OpenAPI spec and start integration-testing REST clients before the wrapper is built. It also prevents the most common mistake in SOAP-to-REST projects — exposing the SOAP operation model verbatim (everything is a POST, request bodies contain a top-level element named after the WSDL operation, namespace prefixes appear in JSON keys). A REST API that looks like SOAP in JSON is a maintenance liability and a developer experience failure.

openapi-payment-initiation.yamlyaml
openapi: '3.1.0'
info:
  title: Payment Initiation API
  version: '1.0.0'
  description: |
    REST facade for the SAIB core payment initiation SOAP service.
    Backed by InitiatePayment SOAP operation (WSDL v2.4).
    All monetary amounts are strings in decimal notation (e.g. "12345.67").

servers:
  - url: https://api.saib.com.sa/v1

security:
  - FAPI2Bearer: []

paths:
  /payments:
    post:
      operationId: initiatePayment
      summary: Initiate a domestic or international payment
      tags: [Payments]
      parameters:
        - name: x-fapi-interaction-id
          in: header
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [debtorIban, creditorIban, amount, currency, endToEndId]
              properties:
                debtorIban:   { type: string, pattern: '^SA[0-9]{2}[0-9A-Z]{18}$' }
                creditorIban: { type: string }
                amount:       { type: string, pattern: '^[0-9]+\.[0-9]{2}$' }
                currency:     { type: string, minLength: 3, maxLength: 3 }
                endToEndId:   { type: string, maxLength: 35 }
                remittanceInformation: { type: string, maxLength: 140 }
      responses:
        '201':
          description: Payment accepted and queued for processing
          headers:
            Location:
              schema: { type: string, format: uri }
              description: URL to retrieve payment status
            x-fapi-interaction-id:
              schema: { type: string }
          content:
            application/json:
              schema:
                type: object
                properties:
                  paymentId:       { type: string }
                  status:          { type: string, enum: [ACCEPTED, PENDING, REJECTED] }
                  transactionDate: { type: string, format: date }
        '400':
          description: Invalid request (maps from SOAP:Client fault)
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }

components:
  securitySchemes:
    FAPI2Bearer:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.saib.com.sa/oauth2/authorize
          tokenUrl:         https://auth.saib.com.sa/oauth2/token
          scopes:
            payments:initiate: Initiate a payment on behalf of the account holder

Strangler Fig Migration

The strangler fig pattern allows a SOAP-to-REST migration without a flag-day cutover. The SOAP backend continues running and continues to serve existing SOAP consumers. The REST facade runs in parallel, serving new consumers. Over time, existing SOAP consumers migrate to REST and the SOAP interface is deprecated but not removed until all consumers have moved.

In IBM API Connect, implement the routing table by creating a single API product that routes based on the incoming transport: SOAP requests (identified by the Content-Type: text/xml header or the presence of a SOAPAction header) are forwarded to the SOAP endpoint unchanged; REST requests (identified by Content-Type: application/json and an OAuth2 Bearer token) are processed through the REST assembly. This single gateway entry point makes the migration transparent to the network routing layer and makes it possible to track which consumers are still using the SOAP path from the APIC analytics dashboard.

Track SOAP consumer migration explicitly. Set a deprecation date for the SOAP endpoint, communicate it to all consumer teams, and verify progress monthly through the gateway access logs. The SOAP endpoint is decommissioned only when APIC analytics shows zero SOAP-path requests over a 60-day window, not on a calendar date that was set before consumer migration was confirmed.

Pitfalls

SOAP arrays need explicit FOR loop handling in ESQL

When a SOAP response contains a list element with maxOccurs="unbounded", the xml-to-json policy converts it to a JSON array only if there are two or more elements. A single element is converted to a JSON object. This breaks any REST client that expects an array. In ACE ESQL, use a FOR loop over the repeating element and explicitly append each item to a JSON array node — this correctly produces a one-element array when there is only one item. The generic xml-to-json policy is not safe for array fields without downstream normalisation.

SOAP backend timeouts are invisible to the REST client

If the SOAP backend takes 30 seconds to respond, the REST client waiting on the ACE HTTP node will also wait 30 seconds — with no progress indication. REST clients expect fast responses or a 202 Accepted pattern for long operations. Set an explicit connectTimeout and requestTimeout on the ACE SOAPRequest node. If the backend exceeds the timeout, return an RFC 7807 response with status: 503 and a Retry-After header. Never let a SOAP backend timeout propagate as a hung connection to the REST client.

Steps to Wrap a SOAP Service with a REST Facade Using ACE

  1. Analyse the WSDL and map operations to REST resources and verbs

    List every WSDL operation. Assign each to a REST resource (noun) and HTTP verb (GET, POST, PUT, PATCH, DELETE). Map every SOAP complexType to a JSON Schema object. Map every SOAP fault code to an HTTP status code. Record this mapping in a table that becomes the authoritative reference for both the wrapper implementation and the OpenAPI spec.

  2. Write the OpenAPI 3.1 specification

    Author the OpenAPI spec without looking at the WSDL after the initial mapping step. Design the REST API as if the SOAP backend did not exist. Include FAPI 2.0 security definitions if the endpoint is consumer-facing under SAMA Open Banking. Generate a mock server from the spec and distribute it to consuming teams immediately.

  3. Build the ACE message flow or APIC assembly

    For ACE: create an HTTP Input node, ESQL compute for envelope construction (including WS-Security header), SOAPRequest or HTTPRequest node targeting the SOAP backend, ESQL compute for envelope stripping and JSON conversion, and HTTP Reply. For APIC: configure the invoke policy, gatewayscript for fault detection, xml-to-json, and map policies as described above.

  4. Configure auth translation

    Validate the incoming OAuth2 Bearer token at the gateway layer. Extract the relevant claims (subject, scope, interaction ID). Construct the SOAP authentication mechanism (UsernameToken, BasicAuth, or mTLS) from internally managed service account credentials. Log the OAuth2 subject in every SOAP call for audit traceability.

  5. Implement and test fault mapping

    Enumerate every distinct fault code the SOAP service returns (test it — the WSDL may not be complete). Verify that each fault code maps to the correct HTTP status and produces a valid RFC 7807 Problem Details JSON body. Test with real fault responses from the SOAP backend, not simulated ones.

  6. Set timeouts and circuit breakers

    Configure explicit connect and request timeouts on the ACE SOAPRequest node. Implement a circuit breaker (ACE ExceptionList node or APIC invoke policy retry configuration) that stops calling a non-responsive SOAP backend and returns 503 immediately after a configurable number of consecutive failures.

  7. Register in APIC, publish to developer portal, and begin strangler fig tracking

    Publish the OpenAPI spec to the APIC developer portal. Set up APIC analytics to distinguish SOAP-path and REST-path traffic. Notify all existing SOAP consumers of the REST facade availability and the SOAP deprecation timeline. Track migration progress monthly.

Migration approach comparison

Approach Effort SOAP feature coverage Auth translation Maintenance burden
ACE ESQL wrapper Medium — 2–4 weeks per service Full: WS-Security, SOAP 1.1 and 1.2, WS-Addressing, complex arrays Manual but fully flexible; any credential type Low — a single ACE flow per service; changes are local
IBM API Connect assembly Low–Medium — 1–2 weeks for simple operations Partial: xml-to-json policy fails on complex arrays and unusual fault structures Policy-based; FAPI 2.0 supported natively Medium — YAML assembly + gatewayscript; harder to debug than ESQL
Spring WS proxy Medium — custom Java code per service Good: Spring WS handles most SOAP mechanics; interceptors for WS-Security Custom Spring Security interceptors; requires Java expertise High — Java codebase per service; separate CI/CD pipeline; not on the IBM platform
OpenAPI Generator client High — generated SOAP client + REST layer Good if the generated client is tested against the real backend Custom, must be wired into the generated client High — generated code drifts from the WSDL; regeneration breaks customisations