Building a Generic Apex Callout Utility for Any External API


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 .


This site uses cookies to offer you a better browsing experience. By browsing this website, you agree to our use of cookies.