afv-library/skills/integration-connectivity-generate/references/callout-patterns.md

22 KiB

Callout Patterns Reference

This document provides detailed implementation patterns for REST and SOAP callouts in Salesforce integrations.

Parent Document: integration-connectivity-generate/SKILL.md Related: event-patterns.md


Table of Contents


REST Callout Patterns

Synchronous REST Callout

Use Case: Need immediate response, NOT triggered from DML

Template: assets/callouts/rest-sync-callout.cls

When to Use:

  • User-initiated actions requiring immediate feedback
  • API calls from Lightning Web Components
  • Scheduled batch jobs needing sequential processing
  • Any non-trigger context where response is needed

When NOT to Use:

  • Triggered from DML operations (triggers, Process Builder, flows)
  • Long-running operations (>10 seconds expected)
  • High-volume batch operations

Implementation

public with sharing class {{ServiceName}}Callout {

    private static final String NAMED_CREDENTIAL = 'callout:{{NamedCredentialName}}';

    public static HttpResponse makeRequest(String method, String endpoint, String body) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(NAMED_CREDENTIAL + endpoint);
        req.setMethod(method);
        req.setHeader('Content-Type', 'application/json');
        req.setTimeout(120000); // 120 seconds max

        if (String.isNotBlank(body)) {
            req.setBody(body);
        }

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

    public static Map<String, Object> get(String endpoint) {
        HttpResponse res = makeRequest('GET', endpoint, null);
        return handleResponse(res);
    }

    public static Map<String, Object> post(String endpoint, Map<String, Object> payload) {
        HttpResponse res = makeRequest('POST', endpoint, JSON.serialize(payload));
        return handleResponse(res);
    }

    public static Map<String, Object> put(String endpoint, Map<String, Object> payload) {
        HttpResponse res = makeRequest('PUT', endpoint, JSON.serialize(payload));
        return handleResponse(res);
    }

    public static Map<String, Object> patch(String endpoint, Map<String, Object> payload) {
        HttpResponse res = makeRequest('PATCH', endpoint, JSON.serialize(payload));
        return handleResponse(res);
    }

    public static void deleteRequest(String endpoint) {
        makeRequest('DELETE', endpoint, null);
    }

    private static Map<String, Object> handleResponse(HttpResponse res) {
        Integer statusCode = res.getStatusCode();

        if (statusCode >= 200 && statusCode < 300) {
            return (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
        } else if (statusCode >= 400 && statusCode < 500) {
            throw new CalloutException('Client Error: ' + statusCode + ' - ' + res.getBody());
        } else if (statusCode >= 500) {
            throw new CalloutException('Server Error: ' + statusCode + ' - ' + res.getBody());
        }

        return null;
    }
}

Key Features

  • Named Credential Integration: Uses callout: syntax for secure authentication
  • Timeout Management: 120-second max timeout (governor limit)
  • HTTP Method Support: GET, POST, PUT, PATCH, DELETE
  • Status Code Handling: Differentiates between client (4xx) and server (5xx) errors
  • JSON Serialization: Automatic JSON handling for request/response bodies

Usage Example

// GET request
try {
    Map<String, Object> data = StripeCallout.get('/v1/customers/cus_123');
    String email = (String) data.get('email');
} catch (CalloutException e) {
    // Handle error
    System.debug(LoggingLevel.ERROR, 'Callout failed: ' + e.getMessage());
}

// POST request
Map<String, Object> payload = new Map<String, Object>{
    'email' => 'customer@example.com',
    'name' => 'John Doe'
};
Map<String, Object> response = StripeCallout.post('/v1/customers', payload);

Asynchronous REST Callout (Queueable)

Use Case: Callouts triggered from DML (triggers, Process Builder)

Template: assets/callouts/rest-queueable-callout.cls

When to Use:

  • Callouts from triggers (REQUIRED - sync callouts fail in triggers)
  • Fire-and-forget operations (no immediate response needed)
  • Bulk operations processing multiple records
  • Long-running API calls (>10 seconds)

Governor Limit Considerations:

  • Max 50 queueable jobs per transaction
  • Queueable can chain to another queueable (max depth varies by org)
  • Callout timeout: 120 seconds

Implementation

public with sharing class {{ServiceName}}QueueableCallout implements Queueable, Database.AllowsCallouts {

    private List<Id> recordIds;
    private String operation;

    public {{ServiceName}}QueueableCallout(List<Id> recordIds, String operation) {
        this.recordIds = recordIds;
        this.operation = operation;
    }

    public void execute(QueueableContext context) {
        if (recordIds == null || recordIds.isEmpty()) {
            return;
        }

        try {
            // Query records
            List<{{ObjectName}}> records = [
                SELECT Id, Name, {{FieldsToSend}}
                FROM {{ObjectName}}
                WHERE Id IN :recordIds
                WITH USER_MODE
            ];

            // Make callout for each record (consider batching)
            for ({{ObjectName}} record : records) {
                makeCallout(record);
            }

        } catch (CalloutException e) {
            // Log callout errors
            System.debug(LoggingLevel.ERROR, 'Callout failed: ' + e.getMessage());
            // Consider: Create error log record, retry logic, notification
        } catch (Exception e) {
            System.debug(LoggingLevel.ERROR, 'Error: ' + e.getMessage());
        }
    }

    private void makeCallout({{ObjectName}} record) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:{{NamedCredentialName}}/{{Endpoint}}');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setTimeout(120000);

        Map<String, Object> payload = new Map<String, Object>{
            'id' => record.Id,
            'name' => record.Name
            // Add more fields
        };
        req.setBody(JSON.serialize(payload));

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

        if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
            // Success - update record status if needed
        } else {
            // Handle error
            throw new CalloutException('API Error: ' + res.getStatusCode());
        }
    }
}

Trigger Integration

trigger OpportunityTrigger on Opportunity (after insert, after update) {
    List<Id> opportunityIds = new List<Id>();

    for (Opportunity opp : Trigger.new) {
        // Only sync closed-won opportunities
        if (opp.StageName == 'Closed Won') {
            opportunityIds.add(opp.Id);
        }
    }

    if (!opportunityIds.isEmpty()) {
        // Enqueue async callout
        System.enqueueJob(new SalesforceQueueableCallout(opportunityIds, 'SYNC'));
    }
}

Bulkification Pattern

For high-volume scenarios, batch multiple callouts:

private void makeCallouts(List<{{ObjectName}}> records) {
    // Batch up to 10 records per callout
    Integer BATCH_SIZE = 10;
    List<Map<String, Object>> batch = new List<Map<String, Object>>();

    for (Integer i = 0; i < records.size(); i++) {
        batch.add(buildPayload(records[i]));

        if (batch.size() == BATCH_SIZE || i == records.size() - 1) {
            // Make single callout with batch
            sendBatch(batch);
            batch.clear();
        }
    }
}

private void sendBatch(List<Map<String, Object>> batch) {
    HttpRequest req = new HttpRequest();
    req.setEndpoint('callout:{{NamedCredentialName}}/batch');
    req.setMethod('POST');
    req.setHeader('Content-Type', 'application/json');
    req.setTimeout(120000);
    req.setBody(JSON.serialize(new Map<String, Object>{'records' => batch}));

    Http http = new Http();
    HttpResponse res = http.send(req);
    // Handle response
}

Retry Handler with Exponential Backoff

Use Case: Handle transient failures with intelligent retry logic

Template: assets/callouts/callout-retry-handler.cls

Retry Strategy:

  • Max Retries: 3 attempts
  • Backoff: Exponential (1s, 2s, 4s)
  • Retry on: 5xx server errors, network timeouts
  • Don't Retry: 4xx client errors (bad request, auth failure)

Implementation

public with sharing class CalloutRetryHandler {

    private static final Integer MAX_RETRIES = 3;
    private static final Integer BASE_DELAY_MS = 1000; // 1 second

    public static HttpResponse executeWithRetry(HttpRequest request) {
        Integer retryCount = 0;
        HttpResponse response;

        while (retryCount < MAX_RETRIES) {
            try {
                Http http = new Http();
                response = http.send(request);

                // Success or client error (4xx) - don't retry
                if (response.getStatusCode() < 500) {
                    return response;
                }

                // Server error (5xx) - retry with backoff
                retryCount++;
                if (retryCount < MAX_RETRIES) {
                    // Exponential backoff: 1s, 2s, 4s
                    Integer delayMs = BASE_DELAY_MS * (Integer) Math.pow(2, retryCount - 1);
                    // Note: Apex doesn't have sleep(), so we schedule retry via Queueable
                    throw new RetryableException('Server error, retry ' + retryCount);
                }

            } catch (CalloutException e) {
                retryCount++;
                if (retryCount >= MAX_RETRIES) {
                    throw e;
                }
            }
        }

        return response;
    }

    public class RetryableException extends Exception {}
}

Queueable Retry Pattern

Since Apex doesn't support Thread.sleep(), implement retry delays using Queueable chaining:

public with sharing class CalloutWithRetryQueueable implements Queueable, Database.AllowsCallouts {

    private HttpRequest request;
    private Integer retryCount;
    private static final Integer MAX_RETRIES = 3;

    public CalloutWithRetryQueueable(HttpRequest req) {
        this(req, 0);
    }

    private CalloutWithRetryQueueable(HttpRequest req, Integer retries) {
        this.request = req;
        this.retryCount = retries;
    }

    public void execute(QueueableContext context) {
        try {
            Http http = new Http();
            HttpResponse res = http.send(request);

            if (res.getStatusCode() >= 500 && retryCount < MAX_RETRIES) {
                // Server error - retry
                System.debug(LoggingLevel.WARN, 'Retry ' + (retryCount + 1) + ' for ' + request.getEndpoint());
                System.enqueueJob(new CalloutWithRetryQueueable(request, retryCount + 1));
            } else if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
                // Success
                handleSuccess(res);
            } else {
                // Client error - don't retry
                handleError(res);
            }

        } catch (CalloutException e) {
            if (retryCount < MAX_RETRIES) {
                System.enqueueJob(new CalloutWithRetryQueueable(request, retryCount + 1));
            } else {
                throw e;
            }
        }
    }

    private void handleSuccess(HttpResponse res) {
        // Process successful response
        System.debug('Callout succeeded: ' + res.getBody());
    }

    private void handleError(HttpResponse res) {
        // Log error
        System.debug(LoggingLevel.ERROR, 'Callout error: ' + res.getStatusCode() + ' - ' + res.getBody());
    }
}

Idempotency Considerations

When implementing retries, ensure API operations are idempotent:

// BAD: Non-idempotent (creates new record on each retry)
POST /api/orders { "item": "Widget", "quantity": 1 }

// GOOD: Idempotent (uses idempotency key)
POST /api/orders
Headers: Idempotency-Key: {{recordId}}-{{timestamp}}
{ "item": "Widget", "quantity": 1 }

SOAP Callout Patterns

WSDL2Apex Process

SOAP integrations in Salesforce use WSDL2Apex to auto-generate Apex classes from WSDL files.

Step-by-Step Process

Step 1: Generate Apex from WSDL

  1. Navigate to SetupApex ClassesGenerate from WSDL
  2. Upload WSDL file or provide URL
  3. Salesforce parses WSDL and generates:
    • Stub class (contains service endpoint and operations)
    • Request/Response classes (for each operation)
    • Type classes (for complex data types)

Step 2: Configure Named Credential

Create a Named Credential for the SOAP endpoint:

<?xml version="1.0" encoding="UTF-8"?>
<NamedCredential xmlns="http://soap.sforce.com/2006/04/metadata">
    <label>{{ServiceName}} SOAP</label>
    <endpoint>https://api.example.com/soap/v1</endpoint>
    <principalType>NamedUser</principalType>
    <protocol>Password</protocol>
    <username>{{Username}}</username>
    <password>{{Password}}</password>
</NamedCredential>

Step 3: Use Generated Classes

Generated classes follow this naming pattern:

  • Stub Class: {{WsdlNamespace}}.{{ServiceName}}
  • Port Type: {{WsdlNamespace}}.{{PortTypeName}}
  • Operations: Methods on the port type class

SOAP Service Implementation

Template: assets/soap/soap-callout-service.cls

Basic SOAP Callout

public with sharing class {{ServiceName}}SoapService {

    public static {{ResponseType}} callService({{RequestType}} request) {
        try {
            // Generated stub class
            {{WsdlGeneratedClass}}.{{PortType}} stub = new {{WsdlGeneratedClass}}.{{PortType}}();

            // Set endpoint (use Named Credential if possible)
            stub.endpoint_x = 'callout:{{NamedCredentialName}}';

            // Set timeout
            stub.timeout_x = 120000;

            // Make the call
            return stub.{{OperationName}}(request);

        } catch (Exception e) {
            System.debug(LoggingLevel.ERROR, 'SOAP Callout Error: ' + e.getMessage());
            throw new CalloutException('SOAP service error: ' + e.getMessage());
        }
    }
}

Example: Weather Service SOAP Callout

WSDL: https://your-external-system.com/service?wsdl (replace with your actual WSDL endpoint)

Generated Classes (example based on a weather service WSDL):

  • GlobalWeatherSoap
  • GetWeatherRequest
  • GetWeatherResponse

Service Implementation:

public with sharing class WeatherService {

    public static String getWeather(String city, String country) {
        try {
            // Initialize SOAP stub
            GlobalWeatherSoap.GlobalWeatherSoap stub =
                new GlobalWeatherSoap.GlobalWeatherSoap();

            // Configure endpoint and timeout
            stub.endpoint_x = 'callout:GlobalWeather_NC';
            stub.timeout_x = 120000;

            // Build request
            GlobalWeatherSoap.GetWeatherRequest req =
                new GlobalWeatherSoap.GetWeatherRequest();
            req.CityName = city;
            req.CountryName = country;

            // Make callout
            GlobalWeatherSoap.GetWeatherResponse res = stub.GetWeather(req);

            return res.GetWeatherResult;

        } catch (System.CalloutException e) {
            System.debug(LoggingLevel.ERROR, 'Weather API callout failed: ' + e.getMessage());
            return null;
        }
    }
}

SOAP Headers and Authentication

For SOAP services requiring custom headers (e.g., WS-Security):

public with sharing class SecureSoapService {

    public static void callServiceWithAuth(String username, String password) {
        // Generated stub
        MyService.MyServiceSoap stub = new MyService.MyServiceSoap();

        // Set endpoint
        stub.endpoint_x = 'callout:MyService_NC';
        stub.timeout_x = 120000;

        // Set SOAP headers for authentication
        stub.inputHttpHeaders_x = new Map<String, String>{
            'SOAPAction' => 'http://tempuri.org/IMyService/MyOperation',
            'Authorization' => 'Basic ' + EncodingUtil.base64Encode(
                Blob.valueOf(username + ':' + password)
            )
        };

        // Make request
        MyService.MyRequest req = new MyService.MyRequest();
        MyService.MyResponse res = stub.MyOperation(req);
    }
}

SOAP Fault Handling

public with sharing class RobustSoapService {

    public static Object callWithFaultHandling() {
        try {
            MyService.MyServiceSoap stub = new MyService.MyServiceSoap();
            stub.endpoint_x = 'callout:MyService_NC';
            stub.timeout_x = 120000;

            MyService.MyRequest req = new MyService.MyRequest();
            return stub.MyOperation(req);

        } catch (System.CalloutException e) {
            // Parse SOAP fault
            String errorMessage = e.getMessage();

            if (errorMessage.contains('faultcode')) {
                // SOAP Fault occurred
                System.debug(LoggingLevel.ERROR, 'SOAP Fault: ' + errorMessage);
                // Extract fault details using XML parsing if needed
            } else {
                // Network/HTTP error
                System.debug(LoggingLevel.ERROR, 'Callout error: ' + errorMessage);
            }

            throw e;
        }
    }
}

Async SOAP Callout (Queueable)

For SOAP callouts triggered from DML:

public with sharing class SoapQueueableCallout implements Queueable, Database.AllowsCallouts {

    private List<Id> recordIds;

    public SoapQueueableCallout(List<Id> recordIds) {
        this.recordIds = recordIds;
    }

    public void execute(QueueableContext context) {
        try {
            // Query records
            List<Account> accounts = [
                SELECT Id, Name, BillingCity, BillingCountry
                FROM Account
                WHERE Id IN :recordIds
                WITH USER_MODE
            ];

            // Initialize SOAP stub
            GlobalWeatherSoap.GlobalWeatherSoap stub =
                new GlobalWeatherSoap.GlobalWeatherSoap();
            stub.endpoint_x = 'callout:GlobalWeather_NC';
            stub.timeout_x = 120000;

            // Process each record
            for (Account acc : accounts) {
                GlobalWeatherSoap.GetWeatherRequest req =
                    new GlobalWeatherSoap.GetWeatherRequest();
                req.CityName = acc.BillingCity;
                req.CountryName = acc.BillingCountry;

                GlobalWeatherSoap.GetWeatherResponse res = stub.GetWeather(req);

                // Update account with weather data
                acc.Weather_Data__c = res.GetWeatherResult;
            }

            // Update records
            update as user accounts;

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

Best Practices

Callout Governor Limits

Limit Value Notes
Max callouts per transaction 100 Batch multiple requests if possible
Max timeout per callout 120 seconds Set explicitly with setTimeout()
Max total timeout per transaction 120 seconds All callouts combined
Max heap size 6 MB (sync) / 12 MB (async) Large responses consume heap

Security Checklist

  • Use Named Credentials for authentication (NEVER hardcode credentials)
  • Minimize OAuth scopes to least privilege
  • Use Certificate-based auth for high-security integrations
  • Validate SSL certificates (don't disable SSL verification)
  • Sanitize user input before including in callout payloads
  • Log callout errors without exposing sensitive data

Error Handling Patterns

try {
    HttpResponse res = makeCallout();
    handleResponse(res);
} catch (System.CalloutException e) {
    // Network error, timeout, SSL error
    logError('Callout failed', e);
} catch (JSONException e) {
    // Malformed JSON response
    logError('JSON parsing failed', e);
} catch (Exception e) {
    // Unexpected error
    logError('Unexpected error', e);
}

Testing Callouts

Use Test.setMock() to mock HTTP responses:

@isTest
private class MyCalloutTest {

    @isTest
    static void testSuccessfulCallout() {
        // Set mock response
        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());

        Test.startTest();
        Map<String, Object> result = MyService.get('/customers/123');
        Test.stopTest();

        System.assertEquals('customer@example.com', result.get('email'));
    }

    // Mock class
    private class MockHttpResponseGenerator implements HttpCalloutMock {
        public HttpResponse respond(HttpRequest req) {
            HttpResponse res = new HttpResponse();
            res.setHeader('Content-Type', 'application/json');
            res.setBody('{"email":"customer@example.com"}');
            res.setStatusCode(200);
            return res;
        }
    }
}