Introduction
If you’ve ever integrated Salesforce with an external API, you’ve probably written some Apex code that looked like this:
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.vendor.com/v1/resource');
req.setMethod('GET');
req.setHeader('Authorization', 'Bearer token');
Http http = new Http();
HttpResponse res = http.send(req);
It works — but after a couple of integrations, you end up with copy-pasted snippets everywhere.
Hardcoded endpoints. Hardcoded headers. Token refresh logic scattered across classes.
That’s exactly what I ran into while integrating with Exact Online. The original utility was tightly coupled with Exact’s OData API, which made it hard to reuse for other systems.
So I decided to refactor everything into a Generic Callout Utility.
Goals
- ✅ One utility class that can call any REST API (Exact, Stripe, Twilio, etc.)
- ✅ Support for GET, POST, PUT, PATCH, DELETE
- ✅ Dynamic headers and body
- ✅ Named Credential support (no hardcoded URLs or tokens)
- ✅ Pluggable AuthProvider (e.g., Exact token refresh, static API key)
- ✅ Easy retries for transient errors
The GenericCalloutUtil
Here’s the core class:
public with sharing class GenericCalloutUtil {
public class RequestOptions {
public String method;
public String baseUrl;
public String namedCredential;
public String path;
public Map<String,String> headers = new Map<String,String>();
public Map<String,String> queryParams = new Map<String,String>();
public String body;
public Blob bodyAsBlob;
public Integer timeoutSeconds = 120;
public Boolean expectBinary = false;
}
public class Response {
public Integer statusCode;
public String body;
public Blob bodyAsBlob;
public Boolean isSuccess;
}
public interface AuthProvider {
void decorate(HttpRequest req);
Boolean refreshAfter401();
}
public static Response send(RequestOptions opts, AuthProvider auth) {
HttpRequest req = new HttpRequest();
String endpoint = buildEndpoint(opts);
req.setEndpoint(endpoint);
req.setMethod(opts.method);
req.setTimeout((opts.timeoutSeconds == null ? 120 : opts.timeoutSeconds) * 1000);
for (String k : opts.headers.keySet()) {
req.setHeader(k, opts.headers.get(k));
}
if (!String.isBlank(opts.body)) {
req.setBody(opts.body);
if (!req.getHeader('Content-Type').contains('json')) {
req.setHeader('Content-Type', 'application/json');
}
}
if (auth != null) auth.decorate(req);
Http http = new Http();
HttpResponse res = http.send(req);
Response r = new Response();
r.statusCode = res.getStatusCode();
r.body = res.getBody();
r.bodyAsBlob = res.getBodyAsBlob();
r.isSuccess = (r.statusCode >= 200 && r.statusCode < 300);
return r;
}
private static String buildEndpoint(RequestOptions opts) {
if (!String.isBlank(opts.namedCredential)) {
return 'callout:' + opts.namedCredential + opts.path;
}
return opts.baseUrl + opts.path;
}
public class CalloutException extends Exception {}
}
Adding Authentication: Exact Online Example
Exact Online uses OAuth2 with access tokens that expire. In my original utility, the code concatenated Access_Token_0__c
.. Access_Token_3__c
and retried on 401.
In the generic world, that’s a pluggable AuthProvider:
public with sharing class ExactOnlineAuthProvider implements GenericCalloutUtil.AuthProvider {
private Exact_Online_Setting__c settings;
public ExactOnlineAuthProvider(Exact_Online_Setting__c s) {
settings = (s == null) ? Exact_Online_Setting__c.getOrgDefaults() : s;
}
public void decorate(HttpRequest req) {
String token = settings.Access_Token_0__c + settings.Access_Token_1__c +
settings.Access_Token_2__c + settings.Access_Token_3__c;
req.setHeader('Authorization', 'Bearer ' + token);
}
public Boolean refreshAfter401() {
settings = ExactUtilityTest.renewAccessToken(settings);
return settings != null;
}
}
Now, any callout can just plug in this provider.
Example Usages
1. Simple GET
GenericCalloutUtil.RequestOptions opts = new GenericCalloutUtil.RequestOptions();
opts.method = 'GET';
opts.baseUrl = 'https://jsonplaceholder.typicode.com';
opts.path = '/posts/1';
GenericCalloutUtil.Response res = GenericCalloutUtil.send(opts, null);
System.debug(res.body);
2. POST with JSON Body + Auth
GenericCalloutUtil.RequestOptions opts = new GenericCalloutUtil.RequestOptions();
opts.method = 'POST';
opts.baseUrl = 'https://api.example.com';
opts.path = '/objects';
opts.headers.put('Authorization', 'Bearer my_token');
opts.body = JSON.serialize(new Map<String,Object>{
'name' => 'Deepak',
'type' => 'Test'
});
GenericCalloutUtil.Response res = GenericCalloutUtil.send(opts, null);
System.debug(res.statusCode + ': ' + res.body);
3. GET with Exact Online Auth
Exact_Online_Setting__c exact = Exact_Online_Setting__c.getOrgDefaults();
GenericCalloutUtil.RequestOptions opts = new GenericCalloutUtil.RequestOptions();
opts.method = 'GET';
opts.baseUrl = exact.Base_URL__c + '/api/v1/' + exact.Division__c;
opts.path = '/inventory/Warehouses';
GenericCalloutUtil.AuthProvider auth = new ExactOnlineAuthProvider(exact);
GenericCalloutUtil.Response res = GenericCalloutUtil.send(opts, auth);
System.debug(res.body);
Why This Matters
- 🔄 Reusable — one class for all integrations
- 🔌 Plug-in Auth — Exact Online, Stripe, Twilio, or your own OAuth2 provider
- 🛠 Extensible — add retries, logging, or custom error handling
- 📉 Less duplication — no more copy-pasted HttpRequest boilerplate
Conclusion
Moving from a vendor-specific utility (like my old ExactOnlineUtility) to a generic callout client makes integrations cleaner, easier to test, and far more maintainable.
Now, whenever Salesforce needs to talk to an external API, I just spin up a RequestOptions
, plug in the right AuthProvider
, and I’m good to go .