Every Salesforce integration that sends or receives data from an external system goes through the same Apex callout pattern. The three classes handle the entire flow: HttpRequest defines what you want to call, Http executes it, and HttpResponse carries the result back. The rules around those three classes — Named Credentials, governor limits, trigger restrictions, and test mocking — are what distinguish Apex HTTP callouts from HTTP calls in other languages.
The Core Pattern
Every Apex HTTP callout follows the same structure:
// The three-class callout pattern
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:MyCredential/api/v1/records');
req.setMethod('GET');
HttpResponse res = new Http().send(req);
if (res.getStatusCode() == 200) {
String body = res.getBody();
// parse body here
} else {
// handle error status
}
For POST requests that send a body:
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:MyCredential/api/v1/records');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody('{"name":"Example","active":true}');
req.setTimeout(8000); // optional — max 10000ms
HttpResponse res = new Http().send(req);
HttpRequest Methods
| Method | Purpose | Notes |
|---|---|---|
| setEndpoint(url) | Set the target URL | Use Named Credential: callout:CredentialName/path |
| setMethod(verb) | HTTP verb | GET, POST, PUT, PATCH, DELETE |
| setHeader(name, value) | Add a request header | Called multiple times for multiple headers |
| setBody(body) | Request body string | For POST, PUT, PATCH — set Content-Type header too |
| setTimeout(ms) | Per-callout timeout | Maximum 10000ms; default is 10000ms if not set |
| setBodyAsBlob(blob) | Binary body | For file uploads or binary payloads |
Parsing the Response
HttpResponse.getBody() returns the response as a String. Parse JSON with either an untyped map or a typed Apex class:
// Untyped — quick and loose
Map<String, Object> data =
(Map<String, Object>) JSON.deserializeUntyped(res.getBody());
String name = (String) data.get('name');
// Typed — cleaner for known schemas
public class RecordWrapper {
public String name;
public Boolean active;
public Integer count;
}
RecordWrapper parsed =
(RecordWrapper) JSON.deserialize(res.getBody(), RecordWrapper.class);
Always check the status code before parsing. Calling JSON.deserialize() on an error response body (which is typically an error message string, not valid JSON for your schema) throws a System.JSONException. Check res.getStatusCode() == 200 first.
Named Credentials
Named Credentials are the correct way to manage external endpoints and authentication in Apex. Create them in Setup → Security → Named Credentials. The Named Credential stores the base URL and authentication configuration. In code, the endpoint becomes callout:CredentialName/path.
// Wrong — URL and token hardcoded in code
req.setEndpoint('https://api.example.com/v2/records');
req.setHeader('Authorization', 'Bearer abc123secrettoken');
// Right — Named Credential handles URL and auth
req.setEndpoint('callout:ExampleAPI/v2/records');
// Salesforce injects the auth header automatically
Authentication options available in Named Credentials:
- No Authentication — endpoint only, no auth header
- Basic — username and password, Base64-encoded
- OAuth 2.0 — token refresh managed by Salesforce
- JWT Token — JSON Web Token authentication
- AWS Signature Version 4 — for AWS services
- Custom via Auth Provider — for non-standard auth flows
When you change the base URL or rotate API credentials, you update the Named Credential in Setup. No code change, no deployment required. The endpoint and credentials never appear in Apex code or version control.
Callout Governor Limits
Three limits apply to Apex callouts per transaction:
| Limit | Value | Impact |
|---|---|---|
| Timeout per callout | 10 seconds max | Set with setTimeout(); uncaught timeout throws System.CalloutException |
| Callout statements per transaction | 100 maximum | Each Http.send() call counts regardless of endpoint |
| Cumulative callout time | 60 seconds | Sum of all callout durations in the transaction |
In bulk contexts — a trigger processing 200 records where each needs an external callout — you cannot make 200 callouts in one synchronous transaction. The 100-callout limit and 60-second cumulative limit define the architecture: use Batch Apex with Database.AllowsCallouts, processing 50–100 records per execute batch.
Callouts from Triggers Are Not Allowed
Apex triggers run in a synchronous DML transaction. HTTP callouts are not permitted in this context. Attempting a callout from a trigger throws:
System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out.
The solution is to move the callout to an asynchronous context. Three options:
@future(callout=true)
The simplest async callout pattern. Annotate a static void method and pass the data it needs as primitive arguments. No sObject parameters.
public class IntegrationService {
@future(callout=true)
public static void sendRecord(Id recordId, String jsonPayload) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:ExternalSystem/records');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(jsonPayload);
HttpResponse res = new Http().send(req);
// handle response
}
}
// In trigger:
IntegrationService.sendRecord(record.Id, JSON.serialize(payload));
Limitations: cannot chain @future methods, cannot return values, cannot pass sObject arguments, limited to 50 concurrent @future calls per org.
Queueable with Database.AllowsCallouts
The preferred modern pattern for async callouts. Supports state passing, chaining, and higher concurrency.
public class SendRecordJob implements Queueable, Database.AllowsCallouts {
private List<Id> recordIds;
public SendRecordJob(List<Id> ids) {
this.recordIds = ids;
}
public void execute(QueueableContext ctx) {
// query, build payload, callout
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:ExternalSystem/batch');
req.setMethod('POST');
req.setBody(JSON.serialize(recordIds));
HttpResponse res = new Http().send(req);
}
}
// In trigger:
System.enqueueJob(new SendRecordJob(affectedIds));
Batch Apex with Database.AllowsCallouts
For processing large volumes of records with callouts. Each execute() call is a separate transaction with its own callout limits.
public class RecordSyncBatch implements Database.Batchable<SObject>,
Database.AllowsCallouts {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, Name FROM Account WHERE Sync_Status__c = \'Pending\''
);
}
public void execute(Database.BatchableContext bc, List<Account> scope) {
// Each execute call: its own 100-callout limit
for (Account acc : scope) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:ExternalSystem/account');
req.setMethod('POST');
req.setBody(JSON.serialize(acc));
HttpResponse res = new Http().send(req);
}
}
public void finish(Database.BatchableContext bc) {}
}
Testing Callouts: HttpCalloutMock
Apex test methods cannot make live HTTP callouts. You must register a mock before executing any code that calls out. Without a mock, the test throws: System.CalloutException: You have uncommitted work pending.
The HttpCalloutMock interface requires one method — respond(HttpRequest req) — that returns a constructed HttpResponse:
public class MockExternalAPI implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setHeader('Content-Type', 'application/json');
res.setBody('{"id":"abc123","status":"success"}');
return res;
}
}
@IsTest
static void testSendRecord() {
Test.setMock(HttpCalloutMock.class, new MockExternalAPI());
Test.startTest();
IntegrationService.sendRecord('001000000000001', '{"name":"Test"}');
Test.stopTest();
// assert results
}
Test.setMock() must be called before the callout is initiated. For @future callout tests, call it before Test.startTest(). For Queueable tests, call it before System.enqueueJob().
StaticResourceCalloutMock
For simple single-callout tests, StaticResourceCalloutMock skips the custom class entirely. Store the JSON response in a Static Resource (Setup → Custom Code → Static Resources), then reference it in the test:
@IsTest
static void testWithStaticResource() {
StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
mock.setStaticResource('ExternalAPIResponse'); // name of Static Resource
mock.setStatusCode(200);
mock.setHeader('Content-Type', 'application/json');
Test.setMock(HttpCalloutMock.class, mock);
Test.startTest();
IntegrationService.sendRecord('001000000000001', '{"name":"Test"}');
Test.stopTest();
}
MultiStaticResourceCalloutMock
For tests that make multiple callouts to different endpoints in sequence, MultiStaticResourceCalloutMock maps each endpoint URL to a separate Static Resource:
MultiStaticResourceCalloutMock multiMock = new MultiStaticResourceCalloutMock();
multiMock.setStaticResource(
'https://api.example.com/accounts', 'AccountResponse'
);
multiMock.setStaticResource(
'https://api.example.com/contacts', 'ContactResponse'
);
multiMock.setStatusCode(200);
multiMock.setHeader('Content-Type', 'application/json');
Test.setMock(HttpCalloutMock.class, multiMock);
SOAP Callouts
SOAP callouts are less common than REST but still present in enterprise integrations with legacy ERP and financial systems. The mechanism differs from REST callouts: instead of constructing HttpRequest manually, you generate typed Apex stub classes from a WSDL document.
Generate stubs in Setup → Apex Classes → Generate from WSDL. Upload the WSDL file and Salesforce generates Apex classes that handle the SOAP envelope construction, serialisation, and parsing. Call the generated stub methods directly in your code — no HttpRequest construction required.
// Generated stub — call like any Apex method
ExternalService.BasicSoap stub = new ExternalService.BasicSoap();
ExternalService.AccountRecord result = stub.getAccount(accountId);
Named Credentials can be used with SOAP callouts to manage the endpoint URL and authentication, just as with REST. Testing SOAP callouts follows the same mock pattern — implement HttpCalloutMock and return a SOAP-formatted XML response body.
Common Integration Patterns
Trigger → Queueable → Callout
The standard pattern for trigger-initiated integrations. Trigger collects affected record IDs, enqueues a Queueable job, Queueable makes the callout outside the DML transaction.
Error Logging and Retry
Log failed callouts to a custom object (Callout_Log__c) with the endpoint, payload, status code, and error body. A Scheduled Apex or flow retries records with status Failed on a schedule. This gives you observable, restartable integrations without losing data on network errors or rate limit responses.
Platform Events as a Queue
For high-volume integrations, publish a Platform Event from the trigger instead of directly enqueueing a job. A Triggered Subscriber flow or Apex trigger on the Platform Event handles the enqueue. This decouples the originating DML from the integration job and gives you replay and monitoring via Event Bus.
For earlier sessions on Apex fundamentals, see Session 73: Anonymous Apex and the Execute Anonymous Window.
Frequently Asked Questions
How do you make an HTTP callout in Apex?
Use three classes: create an HttpRequest, set the endpoint (using a Named Credential: callout:CredentialName/path), set the method, and optionally add headers and a body. Pass the request to new Http().send(request) and handle the HttpResponse. Check getStatusCode() before parsing getBody().
What are Named Credentials in Salesforce?
Named Credentials store external service base URLs and authentication configuration in Salesforce Setup. In code, use callout:CredentialName/path as the endpoint instead of a hardcoded URL. Salesforce resolves the URL and handles auth (OAuth, JWT, Basic, etc.) automatically. Credentials never appear in code, and URL or credential changes require no deployment.
Can you make HTTP callouts from Apex triggers?
No. Triggers run synchronously in a DML transaction and cannot make HTTP callouts. Move callouts to @future(callout=true) for simple fire-and-forget cases, or to a Queueable class implementing Database.AllowsCallouts for full control. Batch Apex with Database.AllowsCallouts handles bulk callout scenarios.
What are the callout limits in Apex?
Three limits per transaction: 10-second maximum timeout per callout, 100 HTTP callout statements maximum per transaction, and 60 seconds cumulative callout time per transaction. Bulk integrations exceeding these limits require Batch Apex or Queueable chaining to distribute callouts across multiple transactions.
How do you test Apex callouts?
Implement the HttpCalloutMock interface with a respond(HttpRequest) method that returns a constructed HttpResponse. Register it with Test.setMock(HttpCalloutMock.class, new YourMock()) before the callout runs. For simple tests, StaticResourceCalloutMock stores the response in a Static Resource with no custom class needed. For multi-callout tests, MultiStaticResourceCalloutMock maps each endpoint URL to a separate Static Resource.