Callouts & Integration

27.1 HTTP callouts

Callout rules:

Endpoints must be authorized via a Named Credential (recommended) or a Remote Site Setting.

No callouts after uncommitted DML in the same transaction (do callouts first, or go async).

Synchronous triggers can't call out — use Queueable/@future(callout=true).

100 callouts per transaction; 120 s cumulative timeout; request/response size up to 6 MB (sync) / 12 MB (async).

Long-running callouts from Visualforce/Aura can use the Continuation class.

public with sharing class WeatherService {
    public class WeatherException extends Exception {}

    public static Decimal currentTemp(String city) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:Weather_API/v1/current?city=' + EncodingUtil.urlEncode(city, 'UTF-8'));
        req.setMethod('GET');
        req.setHeader('Accept', 'application/json');
        req.setTimeout(20000);                       // ms; max 120,000

        HttpResponse res = new Http().send(req);
        if (res.getStatusCode() != 200) {
            throw new WeatherException('Weather API returned ' + res.getStatusCode() + ': ' + res.getStatus());
        }
        Map<String, Object> body = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
        return (Decimal) body.get('tempC');
    }
}

27.2 Named Credentials and External Credentials

The modern model (Winter '23+) separates where from how to authenticate:

Named Credential Endpoint URL, callout options (generate auth header, allow formulas in headers/body), link to an External Credential External Credential

Authentication protocol (OAuth 2.0 client credentials / browser flow / JWT, AWS Signature v4, Custom, Basic via custom headers) and principals (named or per-user)

The actual identity/secret; access granted via permission sets Custom headers e.g., x-api-key: {!$Credential.Weather_Ext_Cred.ApiKey}

Using callout:Weather_API keeps secrets out of code, supports per-environment endpoints, and lets admins rotate credentials without deployments. Users (or the integration user running async jobs) need permission-set access to the External Credential principal.

27.3 JSON serialization

Tip: paste a JSON sample into a JSON-to-Apex generator (e.g., the community JSON2Apex tool) to create DTO classes quickly.

// typed classes (DTOs) — preferred
public class OrderDto {
    public String orderId;
    public Decimal total;
    public List<LineDto> lines;
    public Datetime placedAt;
}
public class LineDto {
    public String sku;
    public Integer qty;
}

String payload = '{"orderId":"A-1","total":99.5,"lines":[{"sku":"X","qty":2}],"placedAt":"2025-03-01T10:00:00Z"}';
OrderDto order = (OrderDto) JSON.deserialize(payload, OrderDto.class);

String out = JSON.serialize(order);                  // compact
String pretty = JSON.serializePretty(order);
String noNulls = JSON.serialize(order, true);        // suppress null fields

// untyped — for dynamic or partial structures
Map<String, Object> m = (Map<String, Object>) JSON.deserializeUntyped(payload);
List<Object> lines = (List<Object>) m.get('lines');
Map<String, Object> firstLine = (Map<String, Object>) lines[0];

// strict parsing: fail on unknown fields
OrderDto strict = (OrderDto) JSON.deserializeStrict(payload, OrderDto.class);

// reserved words as JSON keys (e.g., "number", "type"): rename in the string or use JSONParser
String fixedJson = payload.replace('"number":', '"number_x":');

// streaming generation
JSONGenerator gen = JSON.c
…

27.4 A reusable REST client with POST

public inherited sharing class ErpClient {
    public class ErpException extends Exception {}

    public static String createOrder(OrderDto order) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:ERP/api/orders');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(order, true));
        req.setTimeout(30000);

        HttpResponse res = new Http().send(req);
        Integer code = res.getStatusCode();
        if (code == 201 || code == 200) {
            Map<String, Object> body = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
            return (String) body.get('id');
        } else if (code == 429 || code >= 500) {
            throw new ErpException('Retryable error ' + code);   // caller can retry asynchronously
        }
        throw new ErpException('ERP rejected order: ' + code + ' ' + res.getBody());
    }
}

27.5 SOAP callouts

Generate Apex stubs from a WSDL: *Setup → Apex Classes → Generate from WSDL* (WSDL2Apex). The generated class has a port type with methods you call directly; set endpoint_x to 'callout:My_Named_Cred/path'. Test with WebServiceMock. For complex or unsupported WSDLs, build the XML manually with Dom.Document and send it via HttpRequest.

27.6 Integration patterns

Request–reply (synchronous) Apex callout from LWC controller/Flow action

Platform Event, Queueable callout, outbound message Batch data sync

Batch Apex callouts, Bulk API 2.0 from the external system Standard REST/SOAP APIs, Apex REST (§28), Composite API UI update from data change CDC, Platform Events + empApi

Salesforce Connect (external objects, __x) Large integrations / API management MuleSoft Anypoint Platform