Salesforce · · 28 min read

The Basics of Integrations in Apex

A complete guide to Salesforce integrations — REST, SOAP, Named Credentials, wrapper classes, JSON2Apex, HTTP callouts, WSDL-generated classes, and writing test classes with mock interfaces.

Part 50: The Basics of Integrations in Apex

Welcome back to the Salesforce series. We have spent the last several posts building a strong foundation in Apex — triggers, test classes, SOQL, security, exception handling, and more. All of that work has been focused on logic that runs inside Salesforce. Starting with this post, we step outside the org and into the world of integrations.

Integrations are how Salesforce talks to other systems. Whether you are pulling order data from an ERP, sending customer records to a marketing platform, or syncing information with a custom application, integrations make it happen. This is one of the most in-demand skills for Salesforce developers, and understanding the fundamentals is essential before you start building real projects.

This post covers everything you need to know to get started: what integrations are, the REST and SOAP protocols, how to test APIs with Postman, Named Credentials for secure authentication, wrapper classes for structuring data, and complete code examples for both REST and SOAP callouts — including how to write test classes for them.


What is an Integration?

An integration is a connection between two or more software systems that allows them to exchange data. In the Salesforce context, an integration connects your Salesforce org to an external system — another cloud application, an on-premise database, a third-party API, or a custom-built service.

Why Integrations Matter

No business runs on a single application. A typical enterprise might use Salesforce for CRM, SAP for ERP, Slack for communication, Stripe for payments, and a custom warehouse management system. These systems need to share data to avoid manual entry, reduce errors, and keep information consistent.

Without integrations, someone would need to manually copy data between systems. That does not scale.

Inbound vs Outbound

There are two directions for any integration:

  • Outbound — Salesforce sends data to an external system. Your Apex code makes an HTTP request to an external API. Salesforce is the client.
  • Inbound — An external system sends data to Salesforce. Your org exposes an API endpoint (REST or SOAP) that external systems call. Salesforce is the server.

This post focuses primarily on outbound integrations — making callouts from Apex to external services. Inbound integrations (custom REST and SOAP endpoints in Apex) will be covered in a future post.

Synchronous vs Asynchronous

  • Synchronous — The calling code waits for the response before continuing. The user or process is blocked until the external system responds. Useful when you need the response immediately (e.g., validating an address before saving a record).
  • Asynchronous — The callout happens in the background. The calling code does not wait for a response. Useful for fire-and-forget scenarios, bulk operations, or when the external system is slow. In Salesforce, asynchronous callouts are typically made from @future(callout=true) methods, Queueable classes, or Batch Apex.

Point-to-Point vs Middleware

  • Point-to-Point — Salesforce connects directly to the external system. Simple to set up for one or two integrations, but becomes a maintenance nightmare as the number of systems grows. Every system needs to know how to talk to every other system.
  • Middleware — A middle layer (like MuleSoft, Dell Boomi, or Informatica) sits between systems and handles routing, transformation, and orchestration. Salesforce talks to the middleware, and the middleware talks to everything else. This is the preferred approach for enterprises with many integrations.

Most Salesforce developers start with point-to-point integrations to learn the fundamentals. That is what we will build in this post.


What is REST?

REST stands for Representational State Transfer. It is an architectural style for building web services. REST is not a protocol — it is a set of principles that, when followed, produce APIs that are simple, scalable, and easy to consume.

REST has become the dominant standard for modern web APIs. When someone says “API” today, they almost always mean a REST API.

Core Principles of REST

  1. Statelessness — Each request from a client to a server must contain all the information needed to understand and process the request. The server does not store any client context between requests. Every call is independent.

  2. Resource-Based — Everything is a resource identified by a URL. A resource might be a user, an order, or a product. You interact with resources using standard HTTP methods.

  3. Uniform Interface — All resources are accessed through a consistent interface using standard HTTP methods and status codes. This makes APIs predictable.

  4. Client-Server Separation — The client and server are independent. The client does not need to know how the server stores data, and the server does not need to know how the client displays it.

HTTP Methods

REST uses standard HTTP methods to perform operations on resources:

MethodPurposeExample
GETRetrieve a resourceGet a list of accounts
POSTCreate a new resourceCreate a new contact
PUTReplace a resource entirelyReplace all fields on a record
PATCHUpdate part of a resourceUpdate just the phone number
DELETERemove a resourceDelete a record

HTTP Status Codes

The server responds with a status code that tells the client what happened:

CodeMeaningWhen You See It
200OKRequest succeeded
201CreatedA new resource was created (POST)
204No ContentSuccess, but nothing to return (DELETE)
400Bad RequestThe request was malformed
401UnauthorizedAuthentication failed or missing
403ForbiddenAuthenticated but not authorized
404Not FoundThe resource does not exist
405Method Not AllowedWrong HTTP method for that endpoint
429Too Many RequestsRate limit exceeded
500Internal Server ErrorSomething broke on the server

JSON Payloads

REST APIs almost always use JSON (JavaScript Object Notation) for request and response bodies. JSON is lightweight, human-readable, and easy to parse in virtually every programming language, including Apex.

A typical JSON response from a REST API might look like this:

{
  "id": 12345,
  "name": "Acme Corporation",
  "industry": "Technology",
  "employees": 500,
  "address": {
    "street": "100 Main Street",
    "city": "San Francisco",
    "state": "CA",
    "zip": "94105"
  },
  "contacts": [
    {
      "firstName": "Jane",
      "lastName": "Smith",
      "email": "jane.smith@acme.com"
    }
  ]
}

Apex has built-in support for JSON through the JSON class, which provides serialize() and deserialize() methods. We will use these extensively.


What is SOAP?

SOAP stands for Simple Object Access Protocol. Unlike REST, SOAP is an actual protocol with strict rules for message format, transport, and error handling. SOAP messages are always formatted in XML and follow a specific envelope structure.

WSDL — The Contract

Every SOAP service publishes a WSDL (Web Services Description Language) file. The WSDL is an XML document that describes everything about the service: what operations are available, what parameters each operation accepts, what the response looks like, and where the service lives (its endpoint URL).

Think of the WSDL as a contract. The client reads the WSDL to understand how to call the service. In Salesforce, you can import a WSDL to automatically generate Apex classes that handle the communication for you.

SOAP Envelope Structure

Every SOAP message is wrapped in an envelope:

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:web="http://www.example.com/webservice">
    <soapenv:Header>
        <!-- Optional: authentication tokens, metadata -->
        <web:AuthHeader>
            <web:ApiKey>abc123</web:ApiKey>
        </web:AuthHeader>
    </soapenv:Header>
    <soapenv:Body>
        <!-- The actual request -->
        <web:GetAccountDetails>
            <web:AccountId>12345</web:AccountId>
        </web:GetAccountDetails>
    </soapenv:Body>
</soapenv:Envelope>

The envelope has three parts:

  • Envelope — The root element that identifies this as a SOAP message.
  • Header — Optional. Contains metadata like authentication tokens, transaction IDs, or routing information.
  • Body — Required. Contains the actual request or response data.

When SOAP is Still Used

SOAP is older and more verbose than REST, but it is far from dead. You will encounter SOAP in these situations:

  • Enterprise systems — SAP, Oracle EBS, and many legacy systems expose SOAP APIs exclusively.
  • Financial services — Banks and payment processors often require SOAP for its built-in security features (WS-Security).
  • Salesforce’s own APIs — Salesforce provides both REST and SOAP APIs. The Metadata API, for example, is SOAP-based.
  • Existing integrations — Many organizations have SOAP integrations that have been running for years and are not being rewritten.

REST vs SOAP

Here is a comprehensive comparison of the two approaches:

AspectRESTSOAP
ProtocolArchitectural styleFormal protocol
Data FormatJSON (typically), XMLXML only
TransportHTTP/HTTPSHTTP, SMTP, TCP, JMS
ContractOptional (OpenAPI/Swagger)Required (WSDL)
StatefulnessStateless by designCan be stateful
Payload SizeLightweightHeavier (XML overhead)
Error HandlingHTTP status codesSOAP Fault element
SecurityHTTPS, OAuth, API keysWS-Security, SAML
CachingBuilt-in HTTP cachingNo native caching
Ease of UseSimple, quick to learnSteeper learning curve
ToolingPostman, curl, any HTTP clientSoapUI, generated clients
Browser SupportNative (fetch, XMLHttpRequest)Requires libraries
Best ForModern web/mobile apps, public APIsEnterprise systems, strict contracts

When to choose REST: You are building a new integration with a modern system, need speed and simplicity, or are working with mobile or web clients.

When to choose SOAP: The external system only exposes a SOAP API, you need formal contracts, or you are working with legacy enterprise systems.


How to Use Postman to Test Integrations

Before writing any Apex code, you should test external APIs using a tool like Postman. Postman lets you send HTTP requests and inspect responses without writing code. This helps you understand the API’s behavior, verify authentication, and see the exact JSON or XML structures you will need to parse.

Setting Up Postman

  1. Download and install Postman from postman.com.
  2. Create a free account (required for saving collections).
  3. Open the application and create a new workspace for your Salesforce integrations.

Making Your First Request

  1. Click New and select HTTP Request.
  2. Set the method to GET.
  3. Enter a URL like https://jsonplaceholder.typicode.com/users/1 (a free test API).
  4. Click Send.
  5. Inspect the response body, status code, headers, and response time.

Using Collections

Collections let you organize related API requests into groups. Create a collection for each integration project:

  • GitHub API — All GitHub-related endpoints.
  • Salesforce REST API — Queries, record operations, composite requests.
  • Payment Gateway — Charge creation, refund processing, webhook testing.

Using Environments

Environments let you store variables that change between contexts (development, staging, production). Common variables include:

  • baseUrl — The API’s base URL.
  • accessToken — The OAuth token.
  • instanceUrl — Your Salesforce org’s URL.

You reference these in requests using double curly braces: {{baseUrl}}/api/v1/users.

Testing Salesforce APIs with Postman

To call Salesforce REST APIs from Postman:

  1. Get an access token — Use the OAuth 2.0 flow. In Postman’s Authorization tab, select OAuth 2.0 and configure:
    • Grant Type: Authorization Code or Client Credentials
    • Auth URL: https://login.salesforce.com/services/oauth2/authorize
    • Token URL: https://login.salesforce.com/services/oauth2/token
    • Client ID and Client Secret from your Connected App
  2. Set the instance URL — After authentication, use the returned instance_url as your base URL.
  3. Make API calls — For example, to query accounts:
    • GET {{instanceUrl}}/services/data/v60.0/query?q=SELECT+Id,Name+FROM+Account+LIMIT+5

Postman is invaluable for debugging. When your Apex callout is not working, replicate the exact request in Postman to isolate whether the problem is in your code or in the API itself.


What are Named Credentials?

Named Credentials are a Salesforce feature that stores the URL and authentication details for an external service. Instead of hardcoding endpoints and managing tokens in your Apex code, you reference a Named Credential and Salesforce handles the rest.

Why Named Credentials are Critical

Without Named Credentials, your code would look something like this:

HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/v1/accounts');
req.setHeader('Authorization', 'Bearer ' + getStoredToken());

This is problematic for several reasons:

  • Security — Tokens stored in custom settings or custom metadata can be accessed by admins and exposed in debug logs.
  • Maintenance — If the endpoint changes, you need to update code and redeploy.
  • Compliance — Hardcoded credentials violate most security audit requirements.
  • Remote Site Settings — You still need to add the endpoint to Remote Site Settings manually.

With Named Credentials, the code becomes:

HttpRequest req = new HttpRequest();
req.setEndpoint('callout:My_External_Service/v1/accounts');

Salesforce automatically injects the authentication header and resolves the base URL. No tokens in code. No Remote Site Settings needed (Named Credentials handle that automatically).

Legacy Named Credentials vs the New Model

Salesforce has two models for Named Credentials:

Legacy Named Credentials (still supported but not recommended for new work):

  • A single configuration that combines the endpoint URL and authentication in one place.
  • Supports basic password authentication, OAuth 2.0, and JWT.
  • Simpler to set up but less flexible.

Named Credential + External Credential (the current model):

  • External Credential — Defines the authentication protocol and stores the credential details. This is where you configure OAuth client credentials, JWT settings, or custom headers.
  • Named Credential — References an External Credential and adds the endpoint URL. Multiple Named Credentials can share the same External Credential.
  • Permission Set Mapping — Controls which users can use the credential by mapping it to a permission set.

Setup Steps for the New Model

  1. Create an External Credential:

    • Navigate to Setup, search for “Named Credentials.”
    • Go to the External Credentials tab.
    • Click New and configure the authentication protocol (OAuth 2.0, Custom, etc.).
    • For OAuth, provide the Client ID, Client Secret, Token URL, and scopes.
  2. Create a Named Credential:

    • Go to the Named Credentials tab.
    • Click New and provide a label, name, and the endpoint URL.
    • Select the External Credential you just created.
  3. Create a Permission Set Mapping:

    • On the External Credential, add a Principal (Identity Type: Named Principal or Per User Principal).
    • Assign a Permission Set that your integration user belongs to.
  4. Use it in Apex:

HttpRequest req = new HttpRequest();
req.setEndpoint('callout:My_Named_Credential/api/resource');
req.setMethod('GET');

Http http = new Http();
HttpResponse res = http.send(req);
System.debug(res.getBody());

The callout: prefix tells Salesforce to resolve the URL and inject authentication from the Named Credential named My_Named_Credential.


What is a Wrapper Class?

A wrapper class is a custom Apex class that defines a data structure for holding and transferring data. In the context of integrations, wrapper classes represent the shape of JSON request bodies and response bodies.

When you call an external API that returns JSON, you need to parse that JSON into Apex objects. Wrapper classes give you a clean, strongly-typed way to do that instead of manually navigating maps and lists.

Basic Wrapper Class Example

Suppose an external API returns this JSON:

{
  "id": 101,
  "companyName": "Acme Corp",
  "isActive": true,
  "contactEmail": "info@acme.com"
}

You would create a wrapper class like this:

public class AccountWrapper {
    public Integer id;
    public String companyName;
    public Boolean isActive;
    public String contactEmail;
}

Now you can deserialize the JSON response directly into this class:

String jsonResponse = res.getBody();
AccountWrapper account = (AccountWrapper) JSON.deserialize(
    jsonResponse, AccountWrapper.class
);
System.debug(account.companyName); // Acme Corp

Inner Classes for Request and Response

It is common to define wrapper classes as inner classes within a single outer class, especially when you have both request and response structures:

public class ExternalAccountService {

    public class AccountRequest {
        public String companyName;
        public String contactEmail;
        public String industry;
    }

    public class AccountResponse {
        public Integer id;
        public String companyName;
        public Boolean isActive;
        public String contactEmail;
        public String createdDate;
    }

    public class ErrorResponse {
        public String errorCode;
        public String message;
    }
}

Nested Structures

APIs often return nested JSON. Your wrapper classes should mirror that structure:

{
  "data": {
    "user": {
      "id": 1,
      "name": "Jane Smith",
      "roles": ["admin", "editor"]
    },
    "permissions": {
      "canEdit": true,
      "canDelete": false
    }
  },
  "meta": {
    "requestId": "abc-123",
    "timestamp": "2026-04-27T10:30:00Z"
  }
}
public class ApiResponseWrapper {

    public DataWrapper data;
    public MetaWrapper meta;

    public class DataWrapper {
        public UserWrapper user;
        public PermissionsWrapper permissions;
    }

    public class UserWrapper {
        public Integer id;
        public String name;
        public List<String> roles;
    }

    public class PermissionsWrapper {
        public Boolean canEdit;
        public Boolean canDelete;
    }

    public class MetaWrapper {
        public String requestId;
        public String timestamp;
    }
}

Serialization and Deserialization

Apex provides two approaches:

Typed (strongly-typed) — using wrapper classes:

// Deserialize JSON into a wrapper class
ApiResponseWrapper response = (ApiResponseWrapper) JSON.deserialize(
    jsonString, ApiResponseWrapper.class
);

// Serialize a wrapper class into JSON
AccountRequest req = new AccountRequest();
req.companyName = 'Acme Corp';
req.contactEmail = 'info@acme.com';
String jsonBody = JSON.serialize(req);

Untyped — using maps and lists:

// Deserialize into generic collections
Map<String, Object> result = (Map<String, Object>) JSON.deserializeUntyped(jsonString);
String name = (String) result.get('companyName');

// Navigate nested structures
Map<String, Object> data = (Map<String, Object>) result.get('data');
Map<String, Object> user = (Map<String, Object>) data.get('user');
String userName = (String) user.get('name');

Typed deserialization is almost always preferred. It is cleaner, less error-prone, and catches issues at compile time rather than runtime. Use untyped deserialization only when the JSON structure is dynamic or unknown at compile time.


Using JSON2Apex to Automatically Create Wrapper Classes

Writing wrapper classes by hand for complex JSON structures is tedious and error-prone. The JSON2Apex tool automates this process.

What is JSON2Apex?

JSON2Apex is a free online tool (available at json2apex.herokuapp.com and similar sites) that takes a sample JSON payload and generates the corresponding Apex wrapper class with all the nested inner classes, correct data types, and proper structure.

How to Use It

  1. Copy a sample JSON response from the API documentation or from a Postman response.
  2. Paste it into the JSON2Apex tool.
  3. Enter a class name (e.g., GitHubRepoWrapper).
  4. Click Generate.
  5. Copy the generated Apex class into your Salesforce org.

Example

Given this JSON:

{
  "id": 12345,
  "name": "my-repo",
  "full_name": "octocat/my-repo",
  "owner": {
    "login": "octocat",
    "id": 1,
    "avatar_url": "https://avatars.githubusercontent.com/u/1"
  },
  "private": false,
  "description": "A sample repository",
  "fork": false,
  "created_at": "2024-01-15T10:30:00Z",
  "updated_at": "2026-03-20T14:22:00Z",
  "stargazers_count": 42,
  "language": "Apex"
}

JSON2Apex generates something like:

public class GitHubRepoWrapper {

    public Integer id;
    public String name;
    public String full_name;
    public Owner owner;
    public Boolean private_x; // "private" is a reserved word in Apex
    public String description;
    public Boolean fork;
    public String created_at;
    public String updated_at;
    public Integer stargazers_count;
    public String language;

    public class Owner {
        public String login;
        public Integer id;
        public String avatar_url;
    }
}

When to Customize the Output

The generated code is a starting point, not a finished product. You should customize it in these situations:

  • Reserved words — Apex has reserved words like private, class, object, and group. JSON2Apex usually appends _x to these, but you may want to use the @JsonProperty annotation or manual deserialization for cleaner naming.
  • Date handling — JSON dates come as strings. You may want to convert them to Datetime or Date types after deserialization.
  • Null safety — Add null checks if certain fields may be absent.
  • Field naming — JSON uses snake_case (e.g., full_name), but Apex convention is camelCase. You can rename fields and handle mapping manually.
  • List of objects — If the API returns an array at the top level, wrap it: List<GitHubRepoWrapper> repos = (List<GitHubRepoWrapper>) JSON.deserialize(jsonString, List<GitHubRepoWrapper>.class);

REST Integration Example

Let us build a complete outbound REST integration. We will call an external API, parse the response, and handle errors properly.

Remote Site Setting

Before making any callout, you must whitelist the external domain. Navigate to Setup > Remote Site Settings > New and add the URL of the external service. If you are using Named Credentials, this step is handled automatically.

The Integration Class

public class ExternalAccountService {

    private static final String NAMED_CREDENTIAL = 'callout:External_Account_API';

    // Wrapper classes for the response
    public class AccountResponse {
        public Integer id;
        public String companyName;
        public String industry;
        public Boolean isActive;
        public String contactEmail;
    }

    public class ErrorResponse {
        public String errorCode;
        public String message;
    }

    /**
     * Retrieves an account from the external system by ID.
     */
    public static AccountResponse getAccount(Integer accountId) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(NAMED_CREDENTIAL + '/api/v1/accounts/' + accountId);
        req.setMethod('GET');
        req.setHeader('Accept', 'application/json');
        req.setTimeout(30000); // 30 second timeout

        Http http = new Http();
        HttpResponse res = http.send(req);

        if (res.getStatusCode() == 200) {
            return (AccountResponse) JSON.deserialize(
                res.getBody(), AccountResponse.class
            );
        } else {
            handleError(res);
            return null;
        }
    }

    /**
     * Creates an account in the external system.
     */
    public static AccountResponse createAccount(String companyName, String industry, String email) {
        // Build request body using a wrapper
        Map<String, String> requestBody = new Map<String, String>{
            'companyName' => companyName,
            'industry' => industry,
            'contactEmail' => email
        };

        HttpRequest req = new HttpRequest();
        req.setEndpoint(NAMED_CREDENTIAL + '/api/v1/accounts');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setHeader('Accept', 'application/json');
        req.setTimeout(30000);
        req.setBody(JSON.serialize(requestBody));

        Http http = new Http();
        HttpResponse res = http.send(req);

        if (res.getStatusCode() == 201) {
            return (AccountResponse) JSON.deserialize(
                res.getBody(), AccountResponse.class
            );
        } else {
            handleError(res);
            return null;
        }
    }

    /**
     * Updates an existing account in the external system.
     */
    public static AccountResponse updateAccount(Integer accountId, Map<String, Object> fieldsToUpdate) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(NAMED_CREDENTIAL + '/api/v1/accounts/' + accountId);
        req.setMethod('PATCH');
        req.setHeader('Content-Type', 'application/json');
        req.setHeader('Accept', 'application/json');
        req.setTimeout(30000);
        req.setBody(JSON.serialize(fieldsToUpdate));

        Http http = new Http();
        HttpResponse res = http.send(req);

        if (res.getStatusCode() == 200) {
            return (AccountResponse) JSON.deserialize(
                res.getBody(), AccountResponse.class
            );
        } else {
            handleError(res);
            return null;
        }
    }

    /**
     * Deletes an account from the external system.
     */
    public static Boolean deleteAccount(Integer accountId) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(NAMED_CREDENTIAL + '/api/v1/accounts/' + accountId);
        req.setMethod('DELETE');
        req.setTimeout(30000);

        Http http = new Http();
        HttpResponse res = http.send(req);

        if (res.getStatusCode() == 204) {
            return true;
        } else {
            handleError(res);
            return false;
        }
    }

    /**
     * Handles error responses from the external API.
     */
    private static void handleError(HttpResponse res) {
        Integer statusCode = res.getStatusCode();
        String body = res.getBody();

        System.debug(LoggingLevel.ERROR,
            'API Error — Status: ' + statusCode + ' Body: ' + body
        );

        if (statusCode == 401) {
            throw new CalloutException('Authentication failed. Check Named Credential configuration.');
        } else if (statusCode == 404) {
            throw new CalloutException('Resource not found.');
        } else if (statusCode == 429) {
            throw new CalloutException('Rate limit exceeded. Try again later.');
        } else if (statusCode >= 500) {
            throw new CalloutException('External service error: ' + body);
        } else {
            throw new CalloutException('Unexpected error (' + statusCode + '): ' + body);
        }
    }
}

Calling the Service from a Trigger or Flow

You cannot make callouts directly from a trigger (because triggers run in a transaction context). Use a @future method or Queueable class:

public class AccountCalloutHandler {

    @future(callout=true)
    public static void syncAccountToExternal(String companyName, String industry, String email) {
        try {
            ExternalAccountService.AccountResponse response =
                ExternalAccountService.createAccount(companyName, industry, email);

            if (response != null) {
                System.debug('External account created with ID: ' + response.id);
            }
        } catch (Exception e) {
            System.debug(LoggingLevel.ERROR, 'Callout failed: ' + e.getMessage());
            // In production, log this to a custom object for monitoring
        }
    }
}

SOAP Integration Example

SOAP integrations in Salesforce follow a different workflow. Instead of manually constructing HTTP requests, you import a WSDL file and Salesforce generates Apex classes that handle the XML serialization and deserialization for you.

Step 1: Obtain the WSDL

The external system provides a WSDL file. This is usually available as a URL (e.g., https://api.example.com/service?wsdl) or as a downloadable XML file.

Step 2: Generate Apex from WSDL

  1. Navigate to Setup > Apex Classes.
  2. Click Generate from WSDL.
  3. Upload the WSDL file or paste its content.
  4. Salesforce parses the WSDL and shows you the classes it will generate. Review them.
  5. Click Generate Apex Code.

Salesforce creates classes that represent the service, its operations, and its data types. For example, if the WSDL defines a service called AccountService with an operation called getAccountDetails, Salesforce might generate:

  • AccountServicePort — The main service class with methods for each operation.
  • AccountServiceTypes — Data types used by the service.

Step 3: Add the Endpoint to Remote Site Settings

Navigate to Setup > Remote Site Settings and add the WSDL endpoint URL.

Step 4: Call the Generated Class

public class SoapAccountIntegration {

    public static void getAccountFromExternalSystem(String externalAccountId) {
        // The generated class from the WSDL
        AccountServicePort.AccountService service = new AccountServicePort.AccountService();

        // Set the endpoint (if not hardcoded in the generated class)
        service.endpoint_x = 'https://api.example.com/soap/AccountService';

        // Set timeout
        service.timeout_x = 30000;

        try {
            // Call the SOAP operation
            AccountServiceTypes.AccountDetails result =
                service.getAccountDetails(externalAccountId);

            // Use the result
            System.debug('Account Name: ' + result.companyName);
            System.debug('Industry: ' + result.industry);
            System.debug('Active: ' + result.isActive);

            // Create or update a Salesforce record with the data
            Account sfAccount = new Account(
                Name = result.companyName,
                Industry = result.industry,
                External_Id__c = result.accountId
            );
            upsert sfAccount External_Id__c;

        } catch (Exception e) {
            System.debug(LoggingLevel.ERROR, 'SOAP callout failed: ' + e.getMessage());
        }
    }
}

Calling SOAP from a Future Method

Just like REST callouts, SOAP callouts cannot be made from triggers directly:

public class SoapCalloutHandler {

    @future(callout=true)
    public static void syncAccountViaSoap(String externalAccountId) {
        SoapAccountIntegration.getAccountFromExternalSystem(externalAccountId);
    }
}

Key Differences from REST

  • You do not manually create HttpRequest objects — the generated class handles everything.
  • You do not parse JSON — the generated types handle XML deserialization.
  • Errors come as SOAP faults, which Apex surfaces as exceptions.
  • The generated code can be verbose. Review it to understand the structure, but avoid modifying it directly — regenerate from the WSDL if the service changes.

Creating Test Classes for Integrations

Apex test classes cannot make real HTTP callouts. Salesforce blocks all external calls during test execution. Instead, you use mock classes to simulate the external service’s responses.

There are three approaches: HttpCalloutMock, StaticResourceCalloutMock, and WebServiceMock.

Approach 1: HttpCalloutMock Interface

The HttpCalloutMock interface lets you define a class that returns a fake HttpResponse for any HTTP callout. This is the most common approach for REST integrations.

Step 1: Create the mock class.

@isTest
public class ExternalAccountServiceMock implements HttpCalloutMock {

    private Integer statusCode;
    private String responseBody;

    // Constructor lets you configure the mock for different scenarios
    public ExternalAccountServiceMock(Integer statusCode, String responseBody) {
        this.statusCode = statusCode;
        this.responseBody = responseBody;
    }

    public HttpResponse respond(HttpRequest req) {
        HttpResponse res = new HttpResponse();
        res.setStatusCode(this.statusCode);
        res.setHeader('Content-Type', 'application/json');
        res.setBody(this.responseBody);
        return res;
    }
}

Step 2: Use the mock in your test class.

@isTest
public class ExternalAccountServiceTest {

    @isTest
    static void testGetAccount_Success() {
        // Prepare mock response
        String mockJson = '{"id":101,"companyName":"Acme Corp","industry":"Technology","isActive":true,"contactEmail":"info@acme.com"}';

        Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(200, mockJson));

        Test.startTest();
        ExternalAccountService.AccountResponse result =
            ExternalAccountService.getAccount(101);
        Test.stopTest();

        System.assertNotEquals(null, result, 'Response should not be null');
        System.assertEquals('Acme Corp', result.companyName);
        System.assertEquals('Technology', result.industry);
        System.assertEquals(true, result.isActive);
    }

    @isTest
    static void testGetAccount_NotFound() {
        String mockJson = '{"errorCode":"NOT_FOUND","message":"Account not found"}';

        Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(404, mockJson));

        Test.startTest();
        try {
            ExternalAccountService.getAccount(999);
            System.assert(false, 'Should have thrown an exception');
        } catch (CalloutException e) {
            System.assert(e.getMessage().contains('not found'),
                'Exception should mention resource not found');
        }
        Test.stopTest();
    }

    @isTest
    static void testCreateAccount_Success() {
        String mockJson = '{"id":102,"companyName":"New Corp","industry":"Finance","isActive":true,"contactEmail":"hello@newcorp.com"}';

        Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(201, mockJson));

        Test.startTest();
        ExternalAccountService.AccountResponse result =
            ExternalAccountService.createAccount('New Corp', 'Finance', 'hello@newcorp.com');
        Test.stopTest();

        System.assertNotEquals(null, result);
        System.assertEquals(102, result.id);
        System.assertEquals('New Corp', result.companyName);
    }

    @isTest
    static void testDeleteAccount_Success() {
        Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(204, ''));

        Test.startTest();
        Boolean result = ExternalAccountService.deleteAccount(101);
        Test.stopTest();

        System.assertEquals(true, result);
    }

    @isTest
    static void testGetAccount_ServerError() {
        String mockJson = '{"errorCode":"INTERNAL_ERROR","message":"Something went wrong"}';

        Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(500, mockJson));

        Test.startTest();
        try {
            ExternalAccountService.getAccount(101);
            System.assert(false, 'Should have thrown an exception');
        } catch (CalloutException e) {
            System.assert(e.getMessage().contains('External service error'));
        }
        Test.stopTest();
    }
}

Approach 2: StaticResourceCalloutMock

If you prefer to store mock responses in files rather than hardcoding them in test classes, use StaticResourceCalloutMock. This is useful when response bodies are large or complex.

Step 1: Create a Static Resource.

Upload a file containing the JSON response body as a Static Resource named MockAccountResponse.

Step 2: Use it in your test.

@isTest
public class ExternalAccountServiceStaticTest {

    @isTest
    static void testGetAccountWithStaticResource() {
        StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
        mock.setStaticResource('MockAccountResponse');
        mock.setStatusCode(200);
        mock.setHeader('Content-Type', 'application/json');

        Test.setMock(HttpCalloutMock.class, mock);

        Test.startTest();
        ExternalAccountService.AccountResponse result =
            ExternalAccountService.getAccount(101);
        Test.stopTest();

        System.assertNotEquals(null, result);
    }
}

This approach keeps your test classes cleaner when dealing with large response bodies. The JSON lives in the static resource, not inline in your test code.

Approach 3: WebServiceMock for SOAP

SOAP callouts use the WebServiceMock interface instead of HttpCalloutMock.

Step 1: Create the mock class.

@isTest
public class SoapAccountServiceMock implements WebServiceMock {

    public void doInvoke(
        Object stub,
        Object request,
        Map<String, Object> response,
        String endpoint,
        String soapAction,
        String requestName,
        String responseNamespace,
        String responseName,
        String responseType
    ) {
        // Create a mock response object matching the generated types
        AccountServiceTypes.AccountDetails mockAccount =
            new AccountServiceTypes.AccountDetails();
        mockAccount.accountId = '12345';
        mockAccount.companyName = 'Mock SOAP Corp';
        mockAccount.industry = 'Manufacturing';
        mockAccount.isActive = true;

        // Create the response element matching the generated response wrapper
        AccountServiceTypes.GetAccountDetailsResponse responseElement =
            new AccountServiceTypes.GetAccountDetailsResponse();
        responseElement.result = mockAccount;

        // Put it in the response map using the response type key
        response.put('response_x', responseElement);
    }
}

Step 2: Use it in your test.

@isTest
public class SoapAccountIntegrationTest {

    @isTest
    static void testGetAccountFromExternalSystem() {
        Test.setMock(WebServiceMock.class, new SoapAccountServiceMock());

        Test.startTest();
        SoapAccountIntegration.getAccountFromExternalSystem('12345');
        Test.stopTest();

        // Verify the Salesforce record was created
        List<Account> accounts = [
            SELECT Name, Industry, External_Id__c
            FROM Account
            WHERE External_Id__c = '12345'
        ];
        System.assertEquals(1, accounts.size());
        System.assertEquals('Mock SOAP Corp', accounts[0].Name);
    }
}

Testing Future Methods with Callouts

When your callout is inside a @future(callout=true) method, wrap the call in Test.startTest() and Test.stopTest(). The Test.stopTest() forces the future method to execute synchronously so you can verify the results:

@isTest
public class AccountCalloutHandlerTest {

    @isTest
    static void testSyncAccountToExternal() {
        String mockJson = '{"id":201,"companyName":"Future Corp","industry":"Tech","isActive":true,"contactEmail":"info@future.com"}';

        Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(201, mockJson));

        Test.startTest();
        AccountCalloutHandler.syncAccountToExternal('Future Corp', 'Tech', 'info@future.com');
        Test.stopTest();

        // Future method executed synchronously within Test.startTest/stopTest
        // Verify any DML operations that happened inside the future method
    }
}

Best Practices for Integration Test Classes

  1. Test all status codes — Do not just test the happy path. Test 400, 401, 404, 429, and 500 responses.
  2. Test with realistic data — Use JSON responses that match the real API’s structure.
  3. Use configurable mocks — Design your mock classes to accept different status codes and response bodies through constructors so you can reuse them across tests.
  4. Verify the request — In your mock’s respond method, you can inspect the HttpRequest to verify the endpoint, method, headers, and body are correct.
  5. Test edge cases — Empty response bodies, null fields, malformed JSON, timeout scenarios.

Here is an example of a mock that also validates the request:

@isTest
public class ValidatingMock implements HttpCalloutMock {

    public HttpResponse respond(HttpRequest req) {
        // Validate the request
        System.assert(req.getEndpoint().contains('/api/v1/accounts'),
            'Endpoint should target the accounts API');
        System.assertEquals('POST', req.getMethod(),
            'Method should be POST');
        System.assertEquals('application/json', req.getHeader('Content-Type'),
            'Content-Type should be JSON');

        // Validate the request body
        Map<String, Object> body = (Map<String, Object>)
            JSON.deserializeUntyped(req.getBody());
        System.assert(body.containsKey('companyName'),
            'Request body should contain companyName');

        // Return mock response
        HttpResponse res = new HttpResponse();
        res.setStatusCode(201);
        res.setBody('{"id":999,"companyName":"Validated Corp","industry":"Tech","isActive":true,"contactEmail":"v@test.com"}');
        return res;
    }
}

Key Takeaways

  • Integrations connect Salesforce to external systems. They can be inbound or outbound, synchronous or asynchronous, point-to-point or middleware-based.
  • REST is the modern standard — stateless, JSON-based, and simple. Learn the HTTP methods and status codes.
  • SOAP is older but still relevant for enterprise and legacy systems. It uses XML, WSDL contracts, and a formal envelope structure.
  • Postman is essential for testing APIs before you write Apex. Use collections and environments to stay organized.
  • Named Credentials keep your integrations secure by storing endpoints and authentication outside of code. Always prefer them over hardcoded URLs and tokens.
  • Wrapper classes give you strongly-typed Apex objects for JSON request and response bodies. Use JSON2Apex to generate them from sample JSON.
  • REST callouts use HttpRequest and HttpResponse. Set the endpoint, method, headers, body, and timeout. Handle every status code.
  • SOAP callouts start with importing a WSDL. Salesforce generates the classes. You call methods on the generated service class.
  • Test classes for integrations use Test.setMock() with HttpCalloutMock (REST), StaticResourceCalloutMock (file-based REST), or WebServiceMock (SOAP). Test every scenario — success, errors, edge cases.

What is Next?

In Part 51, we will put everything from this post into practice with a hands-on project: REST Integration Project — Connecting Salesforce to GitHub. We will build a complete integration that calls the GitHub API from Salesforce, retrieves repository data, and stores it in custom objects — complete with Named Credentials, wrapper classes, error handling, and test classes with full coverage. See you there.