All skills
Skillintermediate
Integration Patterns
Calling external APIs from Salesforce.
Claude Code Knowledge Pack7/10/2026
Overview
Integration Patterns
REST API Integration
Outbound REST Callouts
Calling external APIs from Salesforce.
/**
* HTTP Callout Service
* Handles outbound REST API calls with retry logic
*/
public class HttpCalloutService {
private static final Integer MAX_RETRIES = 3;
private static final Integer TIMEOUT_MS = 30000;
/**
* Performs GET request with retry logic
*/
public static HttpResponse doGet(String endpoint, Map<String, String> headers) {
return doCallout('GET', endpoint, headers, null);
}
/**
* Performs POST request with JSON body
*/
public static HttpResponse doPost(String endpoint, Map<String, String> headers, Object body) {
return doCallout('POST', endpoint, headers, JSON.serialize(body));
}
/**
* Generic callout method with retry logic
*/
private static HttpResponse doCallout(
String method,
String endpoint,
Map<String, String> headers,
String body
) {
HttpRequest request = new HttpRequest();
request.setEndpoint(endpoint);
request.setMethod(method);
request.setTimeout(TIMEOUT_MS);
// Set default headers
request.setHeader('Content-Type', 'application/json');
request.setHeader('Accept', 'application/json');
// Set custom headers
if (headers != null) {
for (String key : headers.keySet()) {
request.setHeader(key, headers.get(key));
}
}
// Set body for POST/PUT/PATCH
if (String.isNotBlank(body)) {
request.setBody(body);
}
Http http = new Http();
HttpResponse response;
Integer retryCount = 0;
while (retryCount < MAX_RETRIES) {
try {
response = http.send(request);
// Success or client error - don't retry
if (response.getStatusCode() < 500) {
break;
}
// Server error - retry
retryCount++;
if (retryCount < MAX_RETRIES) {
// Exponential backoff simulation via logging
System.debug('Retry ' + retryCount + ' after server error');
}
} catch (CalloutException e) {
retryCount++;
if (retryCount >= MAX_RETRIES) {
throw e;
}
}
}
return response;
}
}
Named Credentials
Always use Named Credentials for secure credential management.
/**
* External API integration using Named Credentials
*/
public class ExternalApiService {
// Named Credential endpoint (configured in Setup > Named Credentials)
private static final String NAMED_CREDENTIAL = 'callout:External_API';
/**
* Get customer data from external system
*/
public static CustomerResponse getCustomer(String customerId) {
String endpoint = NAMED_CREDENTIAL + '/customers/' + customerId;
HttpResponse response = HttpCalloutService.doGet(endpoint, null);
if (response.getStatusCode() == 200) {
return (CustomerResponse)JSON.deserialize(
response.getBody(),
CustomerResponse.class
);
} else {
throw new IntegrationException(
'Failed to get customer: ' + response.getStatusCode() +
' - ' + response.getBody()
);
}
}
/**
* Create customer in external system
*/
public static CustomerResponse createCustomer(CustomerRequest request) {
String endpoint = NAMED_CREDENTIAL + '/customers';
HttpResponse response = HttpCalloutService.doPost(endpoint, null, request);
if (response.getStatusCode() == 201) {
return (CustomerResponse)JSON.deserialize(
response.getBody(),
CustomerResponse.class
);
} else {
throw new IntegrationException(
'Failed to create customer: ' + response.getBody()
);
}
}
// Request/Response wrapper classes
public class CustomerRequest {
public String name;
public String email;
public String phone;
public Address address;
}
public class CustomerResponse {
public String id;
public String name;
public String email;
public String status;
public DateTime createdAt;
}
public class Address {
public String street;
public String city;
public String state;
public String country;
public String postalCode;
}
public class IntegrationException extends Exception {}
}
Inbound REST API
Exposing Salesforce as a REST API.
/**
* Custom REST API endpoint
* Endpoint: /services/apexrest/accounts
*/
@RestResource(urlMapping='/accounts/*')
global with sharing class AccountRestService {
/**
* GET /services/apexrest/accounts/{id}
* Returns account by ID
*/
@HttpGet
global static AccountWrapper getAccount() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
// Extract ID from URL
String accountId = req.requestURI.substringAfterLast('/');
if (String.isBlank(accountId)) {
res.statusCode = 400;
return new AccountWrapper('Account ID is required', null);
}
try {
Account acc = [
SELECT Id, Name, Industry, AnnualRevenue, BillingCity, BillingCountry
FROM Account
WHERE Id = :accountId
WITH SECURITY_ENFORCED
];
res.statusCode = 200;
return new AccountWrapper(null, acc);
} catch (QueryException e) {
res.statusCode = 404;
return new AccountWrapper('Account not found', null);
}
}
/**
* POST /services/apexrest/accounts
* Creates new account
*/
@HttpPost
global static AccountWrapper createAccount(AccountRequest request) {
RestResponse res = RestContext.response;
// Validate request
if (String.isBlank(request.name)) {
res.statusCode = 400;
return new AccountWrapper('Name is required', null);
}
try {
Account acc = new Account(
Name = request.name,
Industry = request.industry,
AnnualRevenue = request.annualRevenue,
BillingStreet = request.billingStreet,
BillingCity = request.billingCity,
BillingState = request.billingState,
BillingCountry = request.billingCountry,
BillingPostalCode = request.billingPostalCode
);
insert acc;
res.statusCode = 201;
return new AccountWrapper(null, acc);
} catch (DmlException e) {
res.statusCode = 400;
return new AccountWrapper(e.getMessage(), null);
}
}
/**
* PATCH /services/apexrest/accounts/{id}
* Updates existing account
*/
@HttpPatch
global static AccountWrapper updateAccount(AccountRequest request) {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
String accountId = req.requestURI.substringAfterLast('/');
try {
Account acc = [SELECT Id FROM Account WHERE Id = :accountId];
if (String.isNotBlank(request.name)) acc.Name = request.name;
if (String.isNotBlank(request.industry)) acc.Industry = request.industry;
if (request.annualRevenue != null) acc.AnnualRevenue = request.annualRevenue;
update acc;
res.statusCode = 200;
return new AccountWrapper(null, acc);
} catch (QueryException e) {
res.statusCode = 404;
return new AccountWrapper('Account not found', null);
}
}
/**
* DELETE /services/apexrest/accounts/{id}
* Deletes account
*/
@HttpDelete
global static AccountWrapper deleteAccount() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
String accountId = req.requestURI.substringAfterLast('/');
try {
Account acc = [SELECT Id FROM Account WHERE Id = :accountId];
delete acc;
res.statusCode = 200;
return new AccountWrapper('Account deleted', null);
} catch (QueryException e) {
res.statusCode = 404;
return new AccountWrapper('Account not found', null);
}
}
// Wrapper classes
global class AccountWrapper {
public String error;
public Account account;
public AccountWrapper(String error, Account account) {
this.error = error;
this.account = account;
}
}
global class AccountRequest {
public String name;
public String industry;
public Decimal annualRevenue;
public String billingStreet;
public String billingCity;
public String billingState;
public String billingCountry;
public String billingPostalCode;
}
}
Platform Events
Event-Driven Architecture
Platform Events enable loosely-coupled, event-driven integrations.
Publishing Events
/**
* Platform Event: Order_Event__e
* Fields: Order_Id__c, Customer_Id__c, Status__c, Amount__c, Payload__c
*/
public class OrderEventPublisher {
/**
* Publishes order events
* @param orders List of orders to publish
* @return List of publish results
*/
public static List<Database.SaveResult> publishOrderEvents(List orders) {
List<Order_Event__e> events = new List<Order_Event__e>();
for (Order ord : orders) {
events.add(new Order_Event__e(
Order_Id__c = ord.Id,
Customer_Id__c = ord.AccountId,
Status__c = ord.Status,
Amount__c = ord.TotalAmount,
Payload__c = JSON.serialize(new OrderPayload(ord))
));
}
// Publish events
List<Database.SaveResult> results = EventBus.publish(events);
// Check results
for (Integer i = 0; i < results.size(); i++) {
if (!results[i].isSuccess()) {
for (Database.Error err : results[i].getErrors()) {
System.debug(LoggingLevel.ERROR,
'Error publishing event: ' + err.getMessage());
}
}
}
return results;
}
/**
* Publishes single event immediately
*/
public static void publishOrderStatusChange(Id orderId, String newStatus) {
Order_Event__e event = new Order_Event__e(
Order_Id__c = orderId,
Status__c = newStatus
);
Database.SaveResult result = EventBus.publish(event);
if (!result.isSuccess()) {
throw new EventPublishException('Failed to publish order event');
}
}
private class OrderPayload {
public String orderId;
public String orderNumber;
public String status;
public Decimal amount;
public List items;
public OrderPayload(Order ord) {
this.orderId = ord.Id;
this.orderNumber = ord.OrderNumber;
this.status = ord.Status;
this.amount = ord.TotalAmount;
}
}
private class OrderItemPayload {
public String productId;
public Integer quantity;
public Decimal unitPrice;
}
public class EventPublishException extends Exception {}
}
Subscribing to Events (Apex Trigger)
/**
* Platform Event Trigger
* Subscribes to Order_Event__e
*/
trigger OrderEventTrigger on Order_Event__e (after insert) {
OrderEventHandler handler = new OrderEventHandler();
handler.handleEvents(Trigger.new);
}
/**
* Platform Event Handler
*/
public class OrderEventHandler {
public void handleEvents(List<Order_Event__e> events) {
List<Order_Sync__c> syncs = new List<Order_Sync__c>();
List orderIds = new List();
for (Order_Event__e event : events) {
// Track replay ID for debugging
System.debug('Processing event with replay ID: ' + event.ReplayId);
orderIds.add(event.Order_Id__c);
// Create sync record
syncs.add(new Order_Sync__c(
Order_Id__c = event.Order_Id__c,
Status__c = event.Status__c,
Event_Replay_Id__c = String.valueOf(event.ReplayId),
Processed_Date__c = DateTime.now()
));
}
// Process in bulk
if (!syncs.isEmpty()) {
insert syncs;
}
// Call external system asynchronously
if (!orderIds.isEmpty() && !System.isBatch() && !System.isFuture()) {
syncOrdersToExternalSystem(orderIds);
}
}
@future(callout=true)
private static void syncOrdersToExternalSystem(List orderIds) {
// Callout to external system
}
}
Subscribing via CometD (External Systems)
// Node.js CometD client for Platform Events
const cometd = require('cometd');
const jsforce = require('jsforce');
async function subscribeToEvents() {
const conn = new jsforce.Connection({
loginUrl: process.env.SF_LOGIN_URL
});
await conn.login(process.env.SF_USERNAME, process.env.SF_PASSWORD);
const client = new cometd.CometD();
client.configure({
url: conn.instanceUrl + '/cometd/58.0',
requestHeaders: {
Authorization: 'Bearer ' + conn.accessToken
}
});
client.handshake((status) => {
if (status.successful) {
// Subscribe to platform event channel
client.subscribe('/event/Order_Event__e', (message) => {
console.log('Received event:', message.data.payload);
const orderId = message.data.payload.Order_Id__c;
const status = message.data.payload.Status__c;
// Process event
processOrderEvent(orderId, status);
});
}
});
}
Change Data Capture
Subscribing to Change Events
/**
* Change Data Capture Trigger for Account changes
* Object must have CDC enabled in Setup
*/
trigger AccountChangeEventTrigger on AccountChangeEvent (after insert) {
AccountChangeEventHandler handler = new AccountChangeEventHandler();
handler.handleChanges(Trigger.new);
}
/**
* Change Data Capture Handler
*/
public class AccountChangeEventH