Every Salesforce integration developer eventually hits the wall. You are tasked with an enterprise API integration CRM project. You write the HTTP request, execute the code, and immediately see: System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out.
Compound this with the operational headache of maintaining OAuth tokens and parsing a 500-line nested JSON response, and a simple integration becomes a fragile, unmaintainable bottleneck.
This guide provides a production-ready architectural pattern for executing a Salesforce REST API callout. We will solve authentication using Named Credentials, cleanly deserialize complex JSON payloads, and structure our transactions to entirely avoid the uncommitted work exception.
The Root Causes of Callout Failures
Before implementing a solution, it is critical to understand the mechanics of the Salesforce execution context that cause these common integration failures.
The "Uncommitted Work Pending" Exception
Salesforce enforces strict multitenant database protections. When you perform a Data Manipulation Language (DML) operation (insert, update, delete), Salesforce locks the affected records. If you were allowed to make an external HTTP callout while holding these locks, a slow external server could cause your transaction to hold database locks indefinitely, degrading performance for the entire instance.
Therefore, the rule is absolute: You cannot perform a DML operation before a callout within the same transaction.
Authentication Maintenance Overhead
Hardcoding API keys or managing custom token refresh logic in Apex is a security liability and consumes governor limits. When access tokens expire, custom Apex logic must catch the 401 Unauthorized response, request a new token, and retry the original payload. This creates immense structural complexity in your Apex HTTP integration.
JSON Parsing Brittleness
Using JSON.deserializeUntyped() results in a nested Map<String, Object>. Accessing deeply nested keys requires repetitive, error-prone type casting. When the external API schema changes, your code fails at runtime with NullPointerException or TypeException errors rather than catching the issue at compile time.
The Fix: A Resilient Callout Architecture
To solve these issues, we will build a decoupled integration architecture. We will use Salesforce Named Credentials to offload authentication, Apex Strong Typing for JSON parsing, and Queueable Apex to isolate the HTTP transaction from prior DML operations.
Step 1: Configure Salesforce Named Credentials
Instead of handling authorization headers in code, configure a Named Credential. In modern Salesforce setups, this involves two components:
- External Credential: Defines the authentication protocol (e.g., OAuth 2.0, Custom Header) and handles token storage/refreshing automatically.
- Named Credential: Defines the base URL and links to the External Credential.
For this guide, assume we have created a Named Credential with the API Name Secure_API_Gateway.
Step 2: Generate the Apex JSON Wrapper
Create a strongly typed class to represent the expected JSON response. This provides compile-time safety and enables autocompletion in your IDE.
public class UserSyncResponseWrapper {
public String transactionId;
public UserData data;
public Boolean success;
public class UserData {
public String externalId;
public String accountTier;
public List<String> permissions;
}
}
Step 3: Write the Callout Service
Next, we write the service class. Notice that we do not set an Authorization header. By prefixing our endpoint with callout:, the Salesforce platform intercepts the request and injects the tokens securely.
public with sharing class UserServiceIntegration {
public static UserSyncResponseWrapper fetchExternalUser(String externalUserId) {
HttpRequest req = new HttpRequest();
// The callout: prefix routes the request through the Named Credential
req.setEndpoint('callout:Secure_API_Gateway/api/v1/users/' + externalUserId);
req.setMethod('GET');
req.setHeader('Accept', 'application/json');
req.setTimeout(120000); // Maximize timeout for enterprise reliability
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
// Deserializing directly into the wrapper class
return (UserSyncResponseWrapper) JSON.deserialize(
res.getBody(),
UserSyncResponseWrapper.class
);
} else {
// Log the actual response body for debugging
throw new CalloutException('Integration Error: ' + res.getStatusCode() + ' - ' + res.getBody());
}
}
}
Step 4: Isolate DML with Queueable Apex
To resolve the uncommitted work pending error, we must ensure the callout happens in a separate transaction from any DML that triggered it (such as a trigger on a User or Contact record).
We implement the Queueable interface and explicitly add the Database.AllowsCallouts marker interface.
public class UserSyncQueueable implements Queueable, Database.AllowsCallouts {
private String externalUserId;
private Id salesforceRecordId;
public UserSyncQueueable(String externalUserId, Id salesforceRecordId) {
this.externalUserId = externalUserId;
this.salesforceRecordId = salesforceRecordId;
}
public void execute(QueueableContext context) {
try {
// 1. Perform the Callout FIRST
UserSyncResponseWrapper response = UserServiceIntegration.fetchExternalUser(this.externalUserId);
if (response.success != null && response.success) {
// 2. Perform the DML SECOND
Contact contactToUpdate = new Contact(
Id = this.salesforceRecordId,
External_Tier__c = response.data.accountTier
);
update contactToUpdate;
}
} catch (Exception ex) {
// In a production environment, write this to a Custom Object for error tracking
System.debug(LoggingLevel.ERROR, 'Queueable Callout Failed: ' + ex.getMessage());
}
}
}
Step 5: Invoking the Architecture safely
When a user is updated in Salesforce, you can safely enqueue this job. The trigger performs its DML, the transaction commits, and the Queueable job fires in a completely new transaction.
// Example invocation from a Trigger Handler or Controller
public static void handleContactUpdate(Id contactId, String externalId) {
// Other DML can happen here without failing the subsequent callout
System.enqueueJob(new UserSyncQueueable(externalId, contactId));
}
Deep Dive: Why This Architecture Works
This pattern is the gold standard for enterprise API integration CRM projects on the Salesforce platform.
By pushing the authentication layer to Salesforce Named Credentials, you eliminate the need to write custom OAuth refresh logic. If an access token expires, the Salesforce platform pauses your HttpRequest, silently requests a new token from the external authorization server, updates the token store, and resumes your original request. This happens entirely outside of your Apex code limits.
Furthermore, decoupling the process using Queueable Apex ensures transactional integrity. If the external server times out after 119 seconds, your initial DML (which may have updated dozens of records) remains safely committed. You only roll back the specific asynchronous job, which can be easily retried via standard Apex retry mechanisms or custom error logging.
Common Pitfalls and Edge Cases
Handling Reserved Keywords in JSON
Often, external APIs return JSON with keys that are reserved words in Apex, such as limit, date, or currency. You cannot name a variable public String limit; in your wrapper class.
To solve this, use string replacement on the payload before deserialization:
// Replace the exact key string before parsing
String safePayload = res.getBody().replace('"limit":', '"limit_x":');
return (UserSyncResponseWrapper) JSON.deserialize(safePayload, UserSyncResponseWrapper.class);
Ensure your wrapper class reflects the modified key name (public Integer limit_x;).
Writing Test Coverage
Salesforce strictly prohibits actual HTTP callouts during test execution. You must implement the HttpCalloutMock interface. Do not rely on static resources for simple mocks; instead, construct the mock dynamically to test various HTTP status codes.
@isTest
public class UserServiceIntegrationMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
res.setBody('{"success": true, "data": {"externalId": "123", "accountTier": "Gold"}}');
res.setStatusCode(200);
return res;
}
}
In your test method, inject the mock before calling the service: Test.setMock(HttpCalloutMock.class, new UserServiceIntegrationMock());.
Conclusion
Building a robust Salesforce REST API callout requires respecting the platform's multi-tenant limitations. By leveraging Named Credentials for automated authentication handling, strongly typed wrappers for JSON safety, and Queueable Apex for transaction isolation, you eliminate the most common integration failures. This architecture scales cleanly, passes security reviews effortlessly, and ensures your CRM integrations remain highly available.