Skip to main content

Salesforce SOAP API in Apex: WSDL2Apex, Callouts & Best Practices Guide

In plain words: SOAP API integration in Salesforce Apex enables your org to exchange structured, XML-based messages with external enterprise systems. By importing a WSDL document into Salesforce using the built-in WSDL2Apex wizard, the platform auto-generates strongly typed Apex classes that use WebServiceCallout.invoke to send requests and parse responses with minimal manual parsing.

While lightweight REST integrations with JSON payloads dominate modern web APIs, many mission-critical enterprise systems—including legacy ERPs, banking networks, and government data exchanges—rely strictly on the SOAP (Simple Object Access Protocol) standard. Salesforce Apex provides native support for both consuming external SOAP services via WSDL2Apex callouts and exposing custom Apex methods as SOAP web services.

1. Understanding the Two Roles of SOAP in Apex

When working with SOAP in Salesforce development, you encounter two distinct patterns:

  • Exposing Custom Web Services: Using the webservice keyword on static methods inside a global Apex class allows external clients to download your custom WSDL from Salesforce Setup and trigger Apex logic remotely.
  • Consuming External SOAP Services (Callouts): Downloading an external system's WSDL file and generating client-side proxy classes via Setup > Apex Classes > Generate from WSDL to initiate outbound XML web service requests.
360 SOAP Integration Architecture Card:
  • Standard Protocol: XML-based messaging governed by a formal WSDL contract.
  • Apex Callout Method: WebServiceCallout.invoke in generated stub classes.
  • Authentication Standard: WS-Security, HTTP Basic, or OAuth managed securely via Named Credentials.
  • Synchronous Timeout Limit: Maximum 120-second timeout per callout.

2. Consuming an External SOAP Service (Outbound Callout)

When you import an external WSDL document, Salesforce creates an Apex representation of the service. Below is a structured example of how the generated proxy class and consumer service execute an outbound SOAP callout to create a contact record in an external master system.

Step 1: Generated WSDL2Apex Stub Class (ExternalContactSoapService.cls)
// Auto-generated or custom-structured WSDL stub
public with sharing class ExternalContactSoapService {

    public class ContactRequest_element {
        public String firstName;
        public String lastName;
        public String email;
        private String[] firstName_type_info = new String[]{'firstName', 'http://services.external.com/contact', null, '1', '1', 'false'};
        private String[] lastName_type_info = new String[]{'lastName', 'http://services.external.com/contact', null, '1', '1', 'false'};
        private String[] email_type_info = new String[]{'email', 'http://services.external.com/contact', null, '0', '1', 'false'};
        private String[] apex_schema_type_info = new String[]{'http://services.external.com/contact', 'true', 'false'};
        private String[] field_order_type_info = new String[]{'firstName', 'lastName', 'email'};
    }

    public class ContactResponse_element {
        public Boolean success;
        public String externalId;
        public String errorMessage;
        private String[] success_type_info = new String[]{'success', 'http://services.external.com/contact', null, '1', '1', 'false'};
        private String[] externalId_type_info = new String[]{'externalId', 'http://services.external.com/contact', null, '0', '1', 'false'};
        private String[] errorMessage_type_info = new String[]{'errorMessage', 'http://services.external.com/contact', null, '0', '1', 'false'};
        private String[] apex_schema_type_info = new String[]{'http://services.external.com/contact', 'true', 'false'};
        private String[] field_order_type_info = new String[]{'success', 'externalId', 'errorMessage'};
    }

    public class ContactPort {
        // Use Named Credentials endpoint for secure authentication
        public String endpoint_x = 'callout:External_Contact_Service/services/soap/v1';
        public Map<String, String> inputHttpHeaders_x;
        public Integer timeout_x = 60000; // 60-second timeout

        public ContactResponse_element createExternalContact(String firstName, String lastName, String email) {
            ContactRequest_element request_x = new ContactRequest_element();
            request_x.firstName = firstName;
            request_x.lastName = lastName;
            request_x.email = email;

            ContactResponse_element response_x;
            Map<String, ContactResponse_element> response_map_x = new Map<String, ContactResponse_element>();
            response_map_x.put('response_x', response_x);

            // Execute native SOAP callout
            WebServiceCallout.invoke(
                this,
                request_x,
                response_map_x,
                new String[]{
                    endpoint_x,
                    'createContactOperation',
                    'http://services.external.com/contact',
                    'createContactRequest',
                    'http://services.external.com/contact',
                    'createContactResponse',
                    'ExternalContactSoapService.ContactResponse_element'
                }
            );

            response_x = response_map_x.get('response_x');
            return response_x;
        }
    }
}
Step 2: Business Logic Service Class Invoking the SOAP Callout (ContactSyncService.cls)
public with sharing class ContactSyncService {

    /**
     * @description Synchronizes local contact data with the external SOAP endpoint
     */
    public static void syncContact(Id contactId) {
        if (contactId == null) {
            return;
        }

        Contact con = [
            SELECT Id, FirstName, LastName, Email 
            FROM Contact 
            WHERE Id = :contactId 
            WITH USER_MODE 
            LIMIT 1
        ];

        try {
            ExternalContactSoapService.ContactPort client = new ExternalContactSoapService.ContactPort();
            ExternalContactSoapService.ContactResponse_element response = client.createExternalContact(
                con.FirstName,
                con.LastName,
                con.Email
            );

            if (response.success) {
                System.debug(LoggingLevel.INFO, 'External record synced. Target ID: ' + response.externalId);
            } else {
                System.debug(LoggingLevel.ERROR, 'External SOAP rejection: ' + response.errorMessage);
            }
        } catch (System.CalloutException ex) {
            System.debug(LoggingLevel.ERROR, 'SOAP Callout Network or Schema Error: ' + ex.getMessage());
        }
    }
}

3. Exposing Custom SOAP Web Services from Apex

If you need external systems to call into Salesforce via SOAP, you can expose methods by marking them with the webservice keyword inside a global class. Once saved, generate the WSDL from Setup > Apex Classes to provide to external developers.

Example: Exposing an Apex Web Service (AccountSoapApi.cls)
global with sharing class AccountSoapApi {

    global class AccountCreationResult {
        webservice Boolean isSuccess;
        webservice String accountId;
        webservice String message;
    }

    webservice static AccountCreationResult createAccount(String accountName, String industry) {
        AccountCreationResult result = new AccountCreationResult();

        if (String.isBlank(accountName)) {
            result.isSuccess = false;
            result.message = 'Account Name is mandatory.';
            return result;
        }

        try {
            Account acc = new Account(
                Name = accountName.trim(),
                Industry = industry
            );
            insert as user acc;

            result.isSuccess = true;
            result.accountId = acc.Id;
            result.message = 'Account created successfully.';
        } catch (DmlException ex) {
            result.isSuccess = false;
            result.message = 'DML Error: ' + ex.getMessage();
        }

        return result;
    }
}

4. Common Traps & SOAP Best Practices

Integration Trap: Calling Inbound Salesforce SOAP Endpoints Directly from Internal Apex
Writing Apex code that constructs a SOAP client to invoke standard Salesforce APIs (such as login.salesforce.com/services/Soap/c/...) to perform CRUD on the local org is an anti-pattern. Native Apex already has direct, secure, and governor-limit-friendly database access via standard DML statements (insert as user, update as user) and SOQL. Use SOAP callouts strictly for communicating with external third-party systems.
Core Rule: Use Named Credentials to manage authentication and avoid hardcoded URLs, test SOAP callouts using WebServiceMock implementations, and verify WSDL schema compatibility before running WSDL2Apex.
  • Use Named Credentials: Replace hardcoded endpoint URLs with callout:My_Named_Credential to prevent certificate errors, avoid hardcoded secrets, and automate credential rotation.
  • Mock SOAP Responses in Unit Tests: Salesforce strictly prohibits live network callouts during test execution. Always implement the WebServiceMock interface and call Test.setMock(WebServiceMock.class, new MySoapMock()) to achieve required code coverage.
  • Handle WSDL Schema Limitations: The native WSDL2Apex generator does not support complex XML schema constructs (such as <xsd:choice>, multiple nested inheritance, or certain dynamic array structures). For unsupported schemas, construct raw HTTP callouts using HttpRequest and parse the XML envelope using Dom.Document.

Summary

Integrating with SOAP web services remains a critical requirement for connecting Salesforce with enterprise legacy systems. By utilizing the built-in WSDL2Apex generator, securing endpoints via Named Credentials, and implementing defensive error handling with WebServiceMock test frameworks, developers can build scalable, rock-solid XML integrations on the Salesforce platform.