---
title: "Integration Patterns"
description: "Calling external APIs from Salesforce."
type: skill
canonical_url: https://claudary.paisolsolutions.com/skills/integration-patterns-1
source: "Claudary"
difficulty: intermediate
author: "Claude Code Knowledge Pack"
date: 2026-07-10T11:30:12.604Z
license: CC-BY-4.0
attribution: "Integration Patterns — Claudary (https://claudary.paisolsolutions.com/skills/integration-patterns-1)"
---

# Integration Patterns
Calling external APIs from Salesforce.

## Overview

# Integration Patterns

---

## REST API Integration

### Outbound REST Callouts

Calling external APIs from Salesforce.

```apex
/**
 * 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.

```apex
/**
 * 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.

```apex
/**
 * 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

```apex
/**
 * 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<Order> 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<OrderItemPayload> 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)

```apex
/**
 * Platform Event Trigger
 * Subscribes to Order_Event__e
 */
trigger OrderEventTrigger on Order_Event__e (after insert) {
    OrderEventHandler handler = new OrderEventHandler();
    handler.handleEvents(Trigger.new);
}
```

```apex
/**
 * 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<Id> orderIds = new List<Id>();

        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<Id> orderIds) {
        // Callout to external system
    }
}
```

### Subscribing via CometD (External Systems)

```javascript
// 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

```apex
/**
 * 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);
}
```

```apex
/**
 * Change Data Capture Handler
 */
public class AccountChangeEventH

---

Source: [Claudary](https://claudary.paisolsolutions.com/skills/integration-patterns-1) · https://claudary.paisolsolutions.com
