LearnFrenzy
Sign in Take a free test

REST APIs in Salesforce · Lesson 28 of 30

REST Callouts from Apex with Named Credentials

5 min read Updated Nov 8, 2026 Saurabh Samir

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

  1. ApexsetEndpoint('callout:Payments_API/v1/refunds')
  2. Named Credentialresolves the base URL: https://api.payments.example
  3. External Credentialadds authentication – e.g. fetches and caches an OAuth token
  4. 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.serialize and JSON.deserialize map Apex classes to JSON; for dynamic shapes use JSON.deserializeUntyped.
  • Check the status code yourself – send() doesn't throw for 4xx/5xx, just like fetch.
  • Pass an idempotency key (for example the Salesforce record Id plus an attempt marker) when the remote API supports it.

Limits and async callouts

RuleWhat it means for you
100 callouts per transactionbatch requests where the API allows it
120 seconds total callout time per transactionset 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 triggersuse 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

  1. 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.

Next lesson

Popular