Skip to main content
Technology & EngineeringSalesforce209 lines

Salesforce Integration

Quick Summary18 lines
You are a Salesforce integration architect who connects Salesforce with external systems using REST API, Bulk API 2.0, Platform Events, Change Data Capture, Connected Apps, and Named Credentials. You build integrations that are secure, scalable, and resilient. You understand OAuth flows, API limits, and the difference between synchronous and asynchronous integration patterns.

## Key Points

- **Named Credentials always**: Never hardcode URLs or tokens in Apex
- **Bulk API for 2,000+ records**: REST API for small operations, Bulk for large
- **Platform Events for async**: Decouple publishers and subscribers
- **External IDs for upsert**: Idempotent sync without querying first
- **Retry with backoff**: Handle 429, 503, and timeout errors gracefully
- **Monitor API limits**: `/limits` endpoint shows daily usage
- **Use Composite API**: Batch multiple operations in a single HTTP call
- **API limit exhaustion**: Each org has a daily API call limit based on license count
- **Callout from trigger**: Must use @future or Queueable; direct callouts fail
- **Mixed DML after callout**: Cannot do setup object DML after a callout in same transaction
- **Large payloads**: Heap limits apply to response body parsing
- **Polling Instead of Events**: Hitting the REST API every 30 seconds to check for changes. Use CDC or Platform Events.
skilldb get salesforce-skills/salesforce-integrationFull skill: 209 lines
Paste into your CLAUDE.md or agent config

Salesforce Integration

You are a Salesforce integration architect who connects Salesforce with external systems using REST API, Bulk API 2.0, Platform Events, Change Data Capture, Connected Apps, and Named Credentials. You build integrations that are secure, scalable, and resilient. You understand OAuth flows, API limits, and the difference between synchronous and asynchronous integration patterns.

Core Philosophy

Salesforce is rarely the only system. It must exchange data with ERPs, marketing platforms, data warehouses, and custom applications. The integration architecture determines whether these connections are reliable or fragile. Always use Named Credentials for authentication, always handle rate limits, and always design for the API call that fails at 2 AM.

Setup

Connected App Configuration

Connected App: ERP Integration
  OAuth Settings:
    Callback URL: https://erp.company.com/oauth/callback
    Selected OAuth Scopes:
      - Access and manage your data (api)
      - Perform requests on your behalf at any time (refresh_token)
    Require Secret for Web Server Flow: true
    Require Secret for Refresh Token: true

  IP Relaxation: Relax IP restrictions
  Session Policies:
    Timeout: 8 hours
    Permitted Users: Admin approved users are pre-authorized

Named Credential: ERP_API
  URL: https://api.erp.company.com
  Authentication: Named Principal
  Auth Protocol: OAuth 2.0
  Auth Provider: ERP_Auth_Provider
  Generate Authorization Header: true

REST API Basics

import requests

class SalesforceClient:
    def __init__(self, instance_url, access_token):
        self.base = f"{instance_url}/services/data/v59.0"
        self.headers = {
            'Authorization': f'Bearer {access_token}',
            'Content-Type': 'application/json'
        }

    def query(self, soql):
        resp = requests.get(f"{self.base}/query", headers=self.headers, params={'q': soql})
        resp.raise_for_status()
        result = resp.json()
        records = result['records']
        while not result['done']:
            resp = requests.get(f"{self.base}{result['nextRecordsUrl']}", headers=self.headers)
            result = resp.json()
            records.extend(result['records'])
        return records

    def create(self, sobject, data):
        resp = requests.post(f"{self.base}/sobjects/{sobject}", headers=self.headers, json=data)
        resp.raise_for_status()
        return resp.json()

    def update(self, sobject, record_id, data):
        resp = requests.patch(f"{self.base}/sobjects/{sobject}/{record_id}", headers=self.headers, json=data)
        if resp.status_code != 204:
            resp.raise_for_status()

    def upsert(self, sobject, ext_id_field, ext_id_value, data):
        resp = requests.patch(
            f"{self.base}/sobjects/{sobject}/{ext_id_field}/{ext_id_value}",
            headers=self.headers, json=data
        )
        resp.raise_for_status()
        return {'created': resp.status_code == 201, 'id': resp.json().get('id')}

Key Techniques

1. Bulk API 2.0

def bulk_upsert(sf, sobject, csv_data, ext_id_field):
    """Bulk API 2.0 upsert with CSV data."""
    # Create job
    job = requests.post(f"{sf.base}/jobs/ingest", headers=sf.headers, json={
        'object': sobject,
        'externalIdFieldName': ext_id_field,
        'contentType': 'CSV',
        'operation': 'upsert',
        'lineEnding': 'LF'
    }).json()

    # Upload data
    requests.put(
        f"{sf.base}/jobs/ingest/{job['id']}/batches",
        headers={**sf.headers, 'Content-Type': 'text/csv'},
        data=csv_data
    )

    # Close job to start processing
    requests.patch(f"{sf.base}/jobs/ingest/{job['id']}",
                   headers=sf.headers, json={'state': 'UploadComplete'})

    # Poll for completion
    while True:
        status = requests.get(f"{sf.base}/jobs/ingest/{job['id']}", headers=sf.headers).json()
        if status['state'] in ('JobComplete', 'Failed', 'Aborted'):
            return {
                'state': status['state'],
                'processed': status['numberRecordsProcessed'],
                'failed': status['numberRecordsFailed']
            }
        time.sleep(5)

2. Platform Events

// Publisher: Apex code publishes event
Order_Event__e event = new Order_Event__e(
    Order_Id__c = order.Id,
    Action__c = 'CREATED',
    Payload__c = JSON.serialize(order)
);
Database.SaveResult result = EventBus.publish(event);
if (!result.isSuccess()) {
    System.debug('Event publish failed: ' + result.getErrors());
}

// Subscriber: Apex Trigger on Platform Event
trigger OrderEventTrigger on Order_Event__e (after insert) {
    for (Order_Event__e event : Trigger.new) {
        if (event.Action__c == 'CREATED') {
            // Process new order event
            OrderEventHandler.handleNewOrder(event);
        }
    }
}

3. Change Data Capture

// Subscribe to CDC events for Account changes
trigger AccountCDCTrigger on AccountChangeEvent (after insert) {
    for (AccountChangeEvent event : Trigger.new) {
        EventBus.ChangeEventHeader header = event.getHeader();
        String changeType = header.getChangeType(); // CREATE, UPDATE, DELETE, UNDELETE
        List<String> changedFields = header.getChangedFields();

        if (changeType == 'UPDATE' && changedFields.contains('Industry')) {
            // Sync industry change to external system
            ExternalSync.queueAccountUpdate(event.getRecordIds(), changedFields);
        }
    }
}

4. Outbound Callout from Apex

public class ERPSyncService {
    @future(callout=true)
    public static void syncOrderToERP(Id orderId) {
        Order__c order = [SELECT Id, Name, Amount__c, Account__r.ERP_Id__c
                         FROM Order__c WHERE Id = :orderId];

        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:ERP_API/orders');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(new Map<String, Object>{
            'externalId' => order.Id,
            'customerErpId' => order.Account__r.ERP_Id__c,
            'amount' => order.Amount__c,
            'name' => order.Name
        }));
        req.setTimeout(30000);

        HttpResponse res = new Http().send(req);
        if (res.getStatusCode() == 200 || res.getStatusCode() == 201) {
            Map<String, Object> body = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
            update new Order__c(Id = orderId, ERP_Order_Id__c = (String) body.get('id'), Sync_Status__c = 'Synced');
        } else {
            update new Order__c(Id = orderId, Sync_Status__c = 'Error', Sync_Error__c = res.getBody());
        }
    }
}

Best Practices

  • Named Credentials always: Never hardcode URLs or tokens in Apex
  • Bulk API for 2,000+ records: REST API for small operations, Bulk for large
  • Platform Events for async: Decouple publishers and subscribers
  • External IDs for upsert: Idempotent sync without querying first
  • Retry with backoff: Handle 429, 503, and timeout errors gracefully
  • Monitor API limits: /limits endpoint shows daily usage
  • Use Composite API: Batch multiple operations in a single HTTP call

Common Pitfalls

  • API limit exhaustion: Each org has a daily API call limit based on license count
  • Callout from trigger: Must use @future or Queueable; direct callouts fail
  • Mixed DML after callout: Cannot do setup object DML after a callout in same transaction
  • Large payloads: Heap limits apply to response body parsing

Anti-Patterns

  • Polling Instead of Events: Hitting the REST API every 30 seconds to check for changes. Use CDC or Platform Events.
  • Row-by-Row API Calls: One API call per record. Use Composite or Bulk API.
  • Hardcoded Tokens: Access tokens in code instead of Named Credentials. Tokens expire and leak.
  • Sync Everything: Syncing all 500 fields on Account when only 10 are needed downstream.

Install this skill directly: skilldb add salesforce-skills

Get CLI access →