home Home build Tools bug_report Errors menu_book Guides lightbulb Tips smart_toy Prompts extension Extensions folder_open Resources info About
search

Salesforce integrations fall into two categories: outbound (Salesforce calls an external API) and inbound (an external system calls Salesforce). This guide covers both, with a focus on production-ready patterns, credential management, and resilient error handling.

Named Credentials

Never hardcode API endpoints or credentials in Apex code. Named Credentials store the endpoint URL and authentication details securely in Setup, keeping secrets out of your codebase and making credential rotation trivial.

lock

Security Best Practice: Named Credentials abstract authentication completely. Even developers with code access cannot see the stored credentials. They also automatically handle OAuth token refresh.

Apex — Using Named Credentials
// Setup > Named Credentials > New
// Developer Name: MyExternalAPI
// URL: https://api.example.com
// Auth Protocol: OAuth 2.0 (configured in Setup)

public class ExternalApiService {

    private static final String ENDPOINT = 'callout:MyExternalAPI';

    public static HttpResponse get(String path) {
        HttpRequest req = new HttpRequest();
        // Named Credential handles the base URL + auth headers automatically
        req.setEndpoint(ENDPOINT + path);
        req.setMethod('GET');
        req.setHeader('Content-Type', 'application/json');
        req.setHeader('Accept', 'application/json');
        req.setTimeout(30000); // 30 seconds

        return new Http().send(req);
    }

    public static HttpResponse post(String path, String body) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(ENDPOINT + path);
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(body);
        req.setTimeout(30000);

        return new Http().send(req);
    }
}

HTTP Callout Pattern

Build a robust HTTP client with proper response handling, status code checking, and typed return values using the wrapper class pattern.

Apex — Typed API Client
public class CustomerApiClient {

    // Wrapper for API response
    public class ApiResponse {
        public Boolean success;
        public Integer statusCode;
        public String body;
        public String errorMessage;
    }

    // Wrapper matching the API's JSON structure
    public class Customer {
        public String id;
        public String name;
        public String email;
        public String status;
        public Decimal balance;
    }

    public static Customer getCustomer(String customerId) {
        ApiResponse response = makeCallout(
            'GET',
            '/v1/customers/' + EncodingUtil.urlEncode(customerId, 'UTF-8'),
            null
        );

        if (!response.success) {
            throw new CalloutException('Failed to get customer: ' + response.errorMessage);
        }

        return (Customer) JSON.deserialize(response.body, Customer.class);
    }

    public static Customer createCustomer(Customer c) {
        String payload = JSON.serialize(c);
        ApiResponse response = makeCallout('POST', '/v1/customers', payload);

        if (!response.success) {
            throw new CalloutException('Failed to create customer: ' + response.errorMessage);
        }

        return (Customer) JSON.deserialize(response.body, Customer.class);
    }

    private static ApiResponse makeCallout(String method, String path, String body) {
        ApiResponse result = new ApiResponse();

        try {
            HttpRequest req = new HttpRequest();
            req.setEndpoint('callout:CustomerAPI' + path);
            req.setMethod(method);
            req.setHeader('Content-Type', 'application/json');
            req.setTimeout(30000);
            if (body != null) req.setBody(body);

            HttpResponse res = new Http().send(req);
            result.statusCode = res.getStatusCode();
            result.body = res.getBody();

            // 2xx = success
            result.success = result.statusCode >= 200 && result.statusCode < 300;

            if (!result.success) {
                result.errorMessage = 'HTTP ' + result.statusCode + ': ' + res.getBody();
            }

        } catch (System.CalloutException e) {
            result.success = false;
            result.errorMessage = e.getMessage();
            System.debug(LoggingLevel.ERROR, 'Callout failed: ' + e.getMessage());
        }

        return result;
    }
}

Exposing Apex as REST API

Use @RestResource to expose Apex classes as REST endpoints that external systems can call. Your org's connected app manages authentication for inbound calls.

Apex — Custom REST Endpoint
// Accessible at: /services/apexrest/accounts/v1/*
@RestResource(urlMapping='/accounts/v1/*')
global class AccountRestService {

    @HttpGet
    global static Account getAccount() {
        RestRequest req = RestContext.request;
        String accountId = req.requestURI.substringAfterLast('/');

        try {
            return [
                SELECT Id, Name, Industry, AnnualRevenue, Phone
                FROM Account
                WHERE Id = :accountId
                LIMIT 1
            ];
        } catch (QueryException e) {
            RestContext.response.statusCode = 404;
            return null;
        }
    }

    @HttpPost
    global static String createAccount(String name, String industry, Decimal revenue) {
        try {
            Account acc = new Account(
                Name = name,
                Industry = industry,
                AnnualRevenue = revenue
            );
            insert acc;

            RestContext.response.statusCode = 201;
            return acc.Id;

        } catch (DmlException e) {
            RestContext.response.statusCode = 400;
            return e.getMessage();
        }
    }

    @HttpDelete
    global static void deleteAccount() {
        String accountId = RestContext.request.requestURI.substringAfterLast('/');
        Account acc = new Account(Id = accountId);
        delete acc;
        RestContext.response.statusCode = 204;
    }
}

Mocking Callouts in Tests

Apex tests cannot make real HTTP callouts. Use HttpCalloutMock to simulate API responses and test all code paths including errors.

Apex — HttpCalloutMock & Test
// Mock class — simulates a successful API response
@isTest
global class CustomerApiMock implements HttpCalloutMock {
    private Integer statusCode;
    private String body;

    global CustomerApiMock(Integer code, String responseBody) {
        this.statusCode = code;
        this.body = responseBody;
    }

    global HTTPResponse respond(HTTPRequest req) {
        HttpResponse res = new HttpResponse();
        res.setHeader('Content-Type', 'application/json');
        res.setBody(this.body);
        res.setStatusCode(this.statusCode);
        return res;
    }
}

// Test class using the mock
@isTest
private class CustomerApiClientTest {

    @isTest
    static void testGetCustomer_success() {
        String mockBody = '{"id":"C001","name":"Acme Corp","email":"info@acme.com","status":"active","balance":5000}';
        Test.setMock(HttpCalloutMock.class, new CustomerApiMock(200, mockBody));

        Test.startTest();
        CustomerApiClient.Customer customer = CustomerApiClient.getCustomer('C001');
        Test.stopTest();

        System.assertNotEquals(null, customer);
        System.assertEquals('Acme Corp', customer.name);
        System.assertEquals(5000, customer.balance);
    }

    @isTest
    static void testGetCustomer_notFound() {
        Test.setMock(HttpCalloutMock.class, new CustomerApiMock(404, '{"error":"Not found"}'));

        Test.startTest();
        try {
            CustomerApiClient.getCustomer('INVALID');
            System.assert(false, 'Should have thrown');
        } catch (CalloutException e) {
            System.assert(e.getMessage().contains('404'));
        }
        Test.stopTest();
    }
}

Retry & Resilience Patterns

External APIs are unreliable. Build retry logic with exponential backoff using Queueable chaining to handle transient failures gracefully.

warning

Idempotency: Before retrying, ensure the operation is idempotent — retrying a POST that creates a record might create duplicates. Use idempotency keys or check-then-act patterns.

Apex — Retry with Queueable
public class RetryableCalloutJob implements Queueable, Database.AllowsCallouts {
    private String recordId;
    private Integer attempts;
    private static final Integer MAX_ATTEMPTS = 3;

    public RetryableCalloutJob(String recordId, Integer attempts) {
        this.recordId = recordId;
        this.attempts = attempts;
    }

    public void execute(QueueableContext ctx) {
        try {
            // Attempt the callout
            CustomerApiClient.Customer c = CustomerApiClient.getCustomer(recordId);
            // Process successful result...
            System.debug('Success on attempt ' + attempts);

        } catch (Exception e) {
            if (attempts < MAX_ATTEMPTS) {
                // Schedule retry (Queueable chaining)
                System.debug('Attempt ' + attempts + ' failed, retrying...');
                System.enqueueJob(new RetryableCalloutJob(recordId, attempts + 1));
            } else {
                // Max attempts reached — log and alert
                System.debug(LoggingLevel.ERROR, 'All ' + MAX_ATTEMPTS + ' attempts failed for ' + recordId);
                // Insert an error log custom record or send email alert
            }
        }
    }
}
100
Max callouts per transaction
120s
Max callout timeout
6 MB
Max response body size
5
Queueable chain depth (sandbox)