# Callout Patterns Reference This document provides detailed implementation patterns for REST and SOAP callouts in Salesforce integrations. > **Parent Document**: [integration-connectivity-generate/SKILL.md](../SKILL.md) > **Related**: [event-patterns.md](./event-patterns.md) --- ## Table of Contents - [REST Callout Patterns](#rest-callout-patterns) - [Synchronous REST Callout](#synchronous-rest-callout) - [Asynchronous REST Callout (Queueable)](#asynchronous-rest-callout-queueable) - [Retry Handler with Exponential Backoff](#retry-handler-with-exponential-backoff) - [SOAP Callout Patterns](#soap-callout-patterns) - [WSDL2Apex Process](#wsdl2apex-process) - [SOAP Service Implementation](#soap-service-implementation) --- ## 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 ```apex 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 get(String endpoint) { HttpResponse res = makeRequest('GET', endpoint, null); return handleResponse(res); } public static Map post(String endpoint, Map payload) { HttpResponse res = makeRequest('POST', endpoint, JSON.serialize(payload)); return handleResponse(res); } public static Map put(String endpoint, Map payload) { HttpResponse res = makeRequest('PUT', endpoint, JSON.serialize(payload)); return handleResponse(res); } public static Map patch(String endpoint, Map 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 handleResponse(HttpResponse res) { Integer statusCode = res.getStatusCode(); if (statusCode >= 200 && statusCode < 300) { return (Map) 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 ```apex // GET request try { Map 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 payload = new Map{ 'email' => 'customer@example.com', 'name' => 'John Doe' }; Map 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 ```apex public with sharing class {{ServiceName}}QueueableCallout implements Queueable, Database.AllowsCallouts { private List recordIds; private String operation; public {{ServiceName}}QueueableCallout(List 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 payload = new Map{ '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 ```apex trigger OpportunityTrigger on Opportunity (after insert, after update) { List opportunityIds = new List(); 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: ```apex private void makeCallouts(List<{{ObjectName}}> records) { // Batch up to 10 records per callout Integer BATCH_SIZE = 10; List> batch = new List>(); 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> 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{'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 ```apex 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: ```apex 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: ```apex // 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 **Reference**: `assets/soap/wsdl2apex-guide.md` 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 **Setup** → **Apex Classes** → **Generate 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 https://api.example.com/soap/v1 NamedUser Password {{Username}} {{Password}} ``` **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 ```apex 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**: ```apex 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): ```apex 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{ '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 ```apex 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: ```apex public with sharing class SoapQueueableCallout implements Queueable, Database.AllowsCallouts { private List recordIds; public SoapQueueableCallout(List recordIds) { this.recordIds = recordIds; } public void execute(QueueableContext context) { try { // Query records List 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 **Template**: `assets/callouts/http-response-handler.cls` ```apex 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: ```apex @isTest private class MyCalloutTest { @isTest static void testSuccessfulCallout() { // Set mock response Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator()); Test.startTest(); Map 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; } } } ``` --- ## Related Resources - [Event Patterns](./event-patterns.md) - Platform Events and Change Data Capture - [Main Skill Documentation](../SKILL.md) - integration-connectivity-generate overview - [Named Credentials Templates](../assets/named-credentials/) - Authentication templates - [Callout Templates](../assets/callouts/) - Ready-to-use callout patterns