REST APIs in Salesforce · Lesson 28 of 30
REST Callouts from Apex with Named Credentials
Salesforce rarely lives alone: orders go to an ERP, payments to a gateway, addresses to a validation service. From Apex you call those REST APIs with HttpRequest – everything from the earlier chapters (methods, status codes, JSON, auth, retries, idempotency) applies. What's specific to Salesforce is how you store credentials safely and how governor limits shape your code.
In this lesson
- Named Credentials and External Credentials
- A complete callout with JSON and error handling
- Callout limits and async callouts
- Testing with HttpCalloutMock
Credentials: never in code
A Named Credential stores the base URL; its External Credential stores the authentication (OAuth 2.0 client credentials, API key header, Basic, JWT…), and permission sets decide which users may use it. Apex refers to it by name with the callout: prefix – no URLs or secrets in your code, and no Remote Site Settings needed.
How a Named Credential callout works
- ApexsetEndpoint('callout:Payments_API/v1/refunds')
- Named Credentialresolves the base URL: https://api.payments.example
- External Credentialadds authentication – e.g. fetches and caches an OAuth token
- Requestsent over HTTPS; the response returns to Apex
A complete callout
public with sharing class PaymentsClient {
public class RefundRequest {
public String paymentId;
public Integer amount; // minor units, e.g. paise
public String reason;
}
public class RefundResult {
public String id;
public String status;
}
public class PaymentsException extends Exception {}
public static RefundResult refund(RefundRequest body, String idempotencyKey) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Payments_API/v1/refunds');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setHeader('Idempotency-Key', idempotencyKey); // safe to retry
req.setBody(JSON.serialize(body));
req.setTimeout(20000); // milliseconds (max 120,000)
HttpResponse res = new Http().send(req);
Integer status = res.getStatusCode();
if (status == 200 || status == 201) {
return (RefundResult) JSON.deserialize(res.getBody(), RefundResult.class);
}
if (status == 409 || status == 422) {
throw new PaymentsException('Refund rejected: ' + res.getBody()); // business error – show the user
}
throw new PaymentsException('Payments API error ' + status); // 5xx – retry later
}
}
JSON.serializeandJSON.deserializemap Apex classes to JSON; for dynamic shapes useJSON.deserializeUntyped.- Check the status code yourself –
send()doesn't throw for 4xx/5xx, just likefetch. - Pass an idempotency key (for example the Salesforce record Id plus an attempt marker) when the remote API supports it.
Limits and async callouts
| Rule | What it means for you |
|---|---|
| 100 callouts per transaction | batch requests where the API allows it |
| 120 seconds total callout time per transaction | set realistic timeouts |
| No callouts after DML in the same transaction (uncommitted work) | do the callout first, or move it to an async job |
| No synchronous callouts from triggers | use Queueable with Database.AllowsCallouts |
public class RefundJob implements Queueable, Database.AllowsCallouts {
private final Id caseId;
public RefundJob(Id caseId) { this.caseId = caseId; }
public void execute(QueueableContext ctx) {
Case c = [SELECT Id, Payment_Id__c, Refund_Amount__c FROM Case WHERE Id = :caseId WITH USER_MODE];
PaymentsClient.RefundRequest body = new PaymentsClient.RefundRequest();
body.paymentId = c.Payment_Id__c;
body.amount = c.Refund_Amount__c.intValue();
body.reason = 'customer_request';
PaymentsClient.RefundResult result = PaymentsClient.refund(body, 'refund-' + c.Id);
update as user new Case(Id = c.Id, Refund_Status__c = result.status); // DML after the callout is fine
}
}
// from a trigger or handler: System.enqueueJob(new RefundJob(caseId));
Testing with HttpCalloutMock
Apex tests can't make real callouts; you provide a mock that returns a fake response:
@IsTest
private class PaymentsClientTest {
private class SuccessMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
Assert.areEqual('POST', req.getMethod());
Assert.isTrue(req.getEndpoint().endsWith('/v1/refunds'));
Assert.isNotNull(req.getHeader('Idempotency-Key'));
HttpResponse res = new HttpResponse();
res.setStatusCode(201);
res.setHeader('Content-Type', 'application/json');
res.setBody('{"id":"rf_1","status":"pending"}');
return res;
}
}
@IsTest
static void refundSucceeds() {
Test.setMock(HttpCalloutMock.class, new SuccessMock());
PaymentsClient.RefundRequest body = new PaymentsClient.RefundRequest();
body.paymentId = 'pay_1';
body.amount = 49900;
Test.startTest();
PaymentsClient.RefundResult r = PaymentsClient.refund(body, 'refund-test-1');
Test.stopTest();
Assert.areEqual('pending', r.status);
}
}
Integration habits on Salesforce
- Credentials only in Named/External Credentials; access granted by permission set.
- Callouts in Queueable jobs when they follow DML or come from triggers; retry failures with a scheduled job or platform events.
- Log requests and responses (without secrets) to a custom object for support.
- Write mocks for success, 4xx and 5xx paths – Apex needs 75% coverage, but the error paths are what matter.
New to Apex? Our Salesforce Apex course covers classes, SOQL, DML and testing from the start.
Practice
Try it yourself
A trigger on Opportunity calls an external pricing API when the stage changes, and fails with "Callout from triggers are currently not supported". Fix the design.
Show answer
Collect the changed Opportunity Ids in the trigger handler and enqueue one Queueable (implementing
Database.AllowsCallouts) for the batch; it makes the callouts and updates the records. That also keeps the trigger fast and bulk-safe.
Quick check
How does Apex reference a Named Credential?
With the callout: prefix in the endpoint, e.g. callout:Payments_API/v1/refunds.
Can you make a callout after DML in the same transaction?
No – do the callout first or move it to an async job.
How do you test callouts?
Implement HttpCalloutMock and register it with Test.setMock.
Summary
- Store endpoints and auth in Named and External Credentials; call them with callout:.
- Build requests with HttpRequest, check status codes, and map JSON with JSON.serialize/deserialize.
- Respect callout limits with Queueable jobs, and test every path with HttpCalloutMock.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.