How do you write an Apex class that integrates with an external REST API and handles authentication errors gracefully?

How to integrate with an external REST API and handle authentication errors gracefully — including expired tokens, invalid credentials, network issues, and unexpected response codes.
✅ Apex Class for REST Integration With Graceful Error Handling
🔹 Example Scenario
You are calling an external REST API with:
- Bearer Token authentication
- Need to auto-refresh token if it expires
- Need robust error handling for API failures
✅ Apex REST Integration Class (with token refresh & graceful handling)
public with sharing class ExternalApiService {
// Store auth token in custom metadata, custom setting, or named credentials
// Here assuming a Custom Setting named ApiCredentials__c
private static ApiCredentials__c creds = ApiCredentials__c.getOrgDefaults();
// Base API URL
private static final String BASE_URL = 'https://api.example.com/v1/';
/**
* Main method to perform GET request
*/
public static String getData(String endpoint) {
try {
HttpRequest req = buildRequest('GET', endpoint, null);
HttpResponse res = sendRequest(req);
return handleResponse(res, 'GET ' + endpoint);
}
catch (Exception ex) {
System.debug('Unexpected error: ' + ex.getMessage());
throw new CalloutException('An unexpected error occurred: ' + ex.getMessage());
}
}
/**
* Build HTTP request object with headers and payload
*/
private static HttpRequest buildRequest(String method, String endpoint, String body) {
HttpRequest req = new HttpRequest();
req.setMethod(method);
req.setEndpoint(BASE_URL + endpoint);
req.setTimeout(60000); // 60 seconds
// Authorization header
req.setHeader('Authorization', 'Bearer ' + creds.AccessToken__c);
req.setHeader('Content-Type', 'application/json');
if (body != null) {
req.setBody(body);
}
return req;
}
/**
* Core method to send HTTP callout with error fallback
*/
private static HttpResponse sendRequest(HttpRequest req) {
Http http = new Http();
HttpResponse res = http.send(req);
// If unauthorized, try refreshing the token once
if (res.getStatusCode() == 401) {
System.debug('Token expired. Refreshing token…');
Boolean refreshed = refreshToken();
if (refreshed) {
// Retry the original request
req.setHeader('Authorization', 'Bearer ' + creds.AccessToken__c);
res = http.send(req);
}
else {
throw new CalloutException('Authentication failed — unable to refresh token.');
}
}
return res;
}
/**
* Handle API response and throw meaningful errors
*/
private static String handleResponse(HttpResponse res, String action) {
Integer code = res.getStatusCode();
String body = res.getBody();
if (code >= 200 && code < 300) {
return body;
}
else if (code == 400) {
throw new CalloutException('Bad Request during ' + action + ': ' + body);
}
else if (code == 403) {
throw new CalloutException('Forbidden: Check permissions on API.');
}
else if (code == 404) {
throw new CalloutException('Endpoint not found: ' + action);
}
else if (code >= 500) {
throw new CalloutException('Server error from external API: ' + code);
}
else {
throw new CalloutException('Unexpected response (' + code + '): ' + body);
}
}
/**
* Refresh the access token
*/
private static Boolean refreshToken() {
try {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/oauth/token');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
String body = 'client_id=' + creds.ClientId__c +
'&client_secret=' + creds.ClientSecret__c +
'&grant_type=refresh_token' +
'&refresh_token=' + creds.RefreshToken__c;
req.setBody(body);
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> tokenData = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
creds.AccessToken__c = (String) tokenData.get('access_token');
creds.save();
return true;
}
else {
System.debug('Token refresh failed: ' + res.getBody());
return false;
}
}
catch (Exception ex) {
System.debug('Token refresh exception: ' + ex.getMessage());
return false;
}
}
}
JavaScript📝 Full Explanation
Below is a deep explanation of how this Apex class works, suitable for documentation, interview prep, or Salesforce certification.
1. Purpose of the class
When integrating Salesforce with an external REST API, common issues arise:
✔ Token expiration
The access token can expire, causing 401 errors.
✔ Network issues
Timeout, connection failure, SSL issues.
✔ API errors
400 (invalid input),
403 (forbidden),
404 (invalid endpoint),
500/503 (server downtime).
✔ Need for safe fallback
Your apex class should not crash; it should return meaningful and safe error messages.
This class is designed exactly for that.
2. Using HttpRequest and Http classes
Salesforce sends outbound requests using HttpRequest and Http.
Key properties included:
setEndpoint()setMethod()setTimeout(60000)→ prevents timeoutssetHeader('Authorization', 'Bearer ...')setBody()for POST calls
3. Graceful handling of authentication errors
🔹 Step 1: Detect expired token
If API returns 401 Unauthorized, the token is probably expired.
🔹 Step 2: Attempt to refresh token once
Your code calls refreshToken() which:
- Sends a POST request to
/oauth/token - Retrieves a new access_token
- Saves the token into Custom Setting / Metadata
🔹 Step 3: Retry original request
After refreshing token, the system retries the same API call.
🔹 Step 4: If still fails, throw a friendly message
throw new CalloutException('Authentication failed — unable to refresh token.');
JavaScript4. Error-handling strategy
This code handles errors as follows:
| Error Type | Handling |
|---|---|
| 400 Bad Request | Returns detailed message indicating what caused the issue |
| 403 Forbidden | Suggest permissions fix |
| 404 Not Found | Endpoint incorrect |
| 500+ API Server error | Graceful failure message |
| Timeout / Exceptions | Catch block returns safe fallback |
5. Secure Storage of Credentials
Tokens and secrets should NOT be hardcoded.
Best options:
✔ Named Credentials (Recommended)
Automatically handles tokens.
✔ Custom Metadata Types
Good for org-wide settings.
✔ Hierarchy Custom Settings
Editable via UI.
This sample uses Custom Settings.
6. Retrying the request
The logic ensures max 1 retry to avoid loops.
7. Clean, reusable architecture
This class is reusable because:
- Authentication is separate
- Error handling centralized
- Request builder method reusable
- Supports GET, POST, PUT with small changes
Related Posts

How to Automatically create a follow-up Task when a Lead is converted

How You need to update a related child record whenever a parent record’s status changes, but only if the status is “Closed Won.” How would you design this in Apex?
