Skip to main content

SF-9226 · Scenario · Medium

Integrate Salesforce with an external system using the REST API — describe your approach

✓ Verified by Vikas Singhal · Last reviewed 5/19/2026 · Updated for Spring '26

A senior REST integration answer covers four things the textbook answer doesn’t: authentication via Named Credentials (not hard-coded tokens), idempotency on retry, governor limits (especially callout-in-trigger rules), and direction (whether Salesforce is the caller or the callee). Interviewers know which corners get cut at junior level.

The 60-second answer

For outbound (Salesforce calls the external system), set up an External Credential + Named Credential for OAuth or API-key auth, then use Http and HttpRequest in an Apex class. For inbound (external system calls Salesforce), create a Connected App with OAuth 2.0, expose either standard sObject REST endpoints (/services/data/vXX.X/sobjects/...) or a custom Apex REST class (@RestResource). For high-volume or async, use Platform Events or Change Data Capture instead of synchronous callouts. Always design for retry idempotency and never callout from inside a trigger directly.

Outbound: Salesforce calls the external system

Step 1 — Named Credential + External Credential

Never hard-code tokens or basic-auth credentials in Apex. Use the platform’s credential system, which gives you encrypted storage, OAuth refresh handling, and a single audit point.

Setup → Named Credentials → New Named Credential
  Label:        Acme Payments API
  Name:         Acme_Payments
  URL:          https://api.acme.com
  External Credential: Acme_Payments_OAuth
    Authentication Protocol: OAuth 2.0
    Identity Type:           Per User / Named Principal
    Scope:                   payments.read payments.write
  Allowed Namespaces, Callout Options, etc.

Then in Apex, the auth is automatic:

HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Acme_Payments/v1/charges');  // ← Named Credential prefix
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(payload));
req.setTimeout(60000);  // 60s — max is 120s

HttpResponse res = new Http().send(req);

The callout:Acme_Payments prefix tells the platform to inject the auth header. No token in source. No manual refresh logic. OAuth refresh is handled by the platform.

Step 2 — Apex callout class

public with sharing class PaymentService {

    public class PaymentResult {
        public Boolean success;
        public String chargeId;
        public String errorMessage;
    }

    public static PaymentResult charge(Decimal amount, String currency, String customerId) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:Acme_Payments/v1/charges');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setHeader('Idempotency-Key', generateIdempotencyKey(customerId, amount));
        req.setBody(JSON.serialize(new Map<String, Object>{
            'amount'      => amount,
            'currency'    => currency,
            'customer_id' => customerId
        }));
        req.setTimeout(60000);

        HttpResponse res;
        PaymentResult result = new PaymentResult();
        try {
            res = new Http().send(req);
            if (res.getStatusCode() == 200 || res.getStatusCode() == 201) {
                Map<String, Object> body = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
                result.success  = true;
                result.chargeId = (String) body.get('id');
            } else {
                result.success      = false;
                result.errorMessage = res.getStatusCode() + ': ' + res.getBody();
            }
        } catch (CalloutException ex) {
            result.success      = false;
            result.errorMessage = 'Callout failed: ' + ex.getMessage();
        }
        return result;
    }

    private static String generateIdempotencyKey(String customerId, Decimal amount) {
        return EncodingUtil.convertToHex(
            Crypto.generateDigest('SHA-256',
                Blob.valueOf(customerId + '|' + amount + '|' + Date.today()))
        );
    }
}

The Idempotency-Key header is essential — if the platform retries the callout, the external system can deduplicate by key instead of charging the customer twice.

Step 3 — No callouts inside triggers

You cannot call Http.send() directly from a trigger. The platform blocks it. To make a callout in response to a record change:

trigger PaymentTrigger on Payment__c (after insert) {
    Set<Id> paymentIds = Trigger.newMap.keySet();
    System.enqueueJob(new PaymentCalloutQueueable(paymentIds));
}

public class PaymentCalloutQueueable implements Queueable, Database.AllowsCallouts {
    private Set<Id> paymentIds;
    public PaymentCalloutQueueable(Set<Id> ids) { this.paymentIds = ids; }
    public void execute(QueueableContext ctx) {
        for (Payment__c p : [SELECT Id, Amount__c, Currency__c, Customer_Id__c
                             FROM Payment__c WHERE Id IN :paymentIds]) {
            PaymentService.charge(p.Amount__c, p.Currency__c, p.Customer_Id__c);
        }
    }
}

Database.AllowsCallouts on the Queueable is required, otherwise the runtime throws when you try to send.

Inbound: external system calls Salesforce

Option A — standard REST API

The external system authenticates with OAuth 2.0 against a Connected App, then calls standard endpoints:

POST /services/data/v60.0/sobjects/Account/
{
  "Name": "Acme Corp",
  "BillingCity": "Phoenix"
}

Use this when the external system needs basic CRUD on standard or custom objects. No Apex required.

Option B — custom Apex REST endpoint

For complex business logic — multi-object transactions, custom validation, response shaping — expose a custom endpoint:

@RestResource(urlMapping='/payments/v1/*')
global with sharing class PaymentRestEndpoint {

    @HttpPost
    global static String createPayment() {
        RestRequest req = RestContext.request;
        Map<String, Object> body = (Map<String, Object>) JSON.deserializeUntyped(req.requestBody.toString());

        Payment__c p = new Payment__c(
            Amount__c   = (Decimal) body.get('amount'),
            Currency__c = (String)  body.get('currency'),
            External_Id__c = (String) body.get('external_id')
        );
        upsert p External_Id__c;  // ← idempotent on retry

        RestContext.response.statusCode = 201;
        return p.Id;
    }
}

The upsert ... External_Id__c makes the endpoint idempotent — if the external system retries with the same external_id, the second request updates instead of duplicating.

Async patterns

For high-volume integration, switch from synchronous callouts to event-driven:

NeedTool
Salesforce → external, fire-and-forgetPlatform Event published by Salesforce, external subscribes via CometD / Pub/Sub API
External → Salesforce, fire-and-forgetExternal publishes Platform Event via REST, Salesforce subscribes via Trigger on the event
Stream all Salesforce data changesChange Data Capture (CDC) — Salesforce publishes change events automatically
Bulk one-time data syncBulk API 2.0 ingest

Sync callouts are bounded by the 100-callouts-per-transaction governor; async patterns scale far beyond that.

Anti-patterns

  • Hard-coded tokens or basic-auth in Apex — security and audit nightmare. Always use Named Credentials.
  • Sync callout from a trigger — blocked by the platform. Use Queueable + Database.AllowsCallouts.
  • No idempotency key — every retry creates a duplicate charge / record / shipment.
  • No timeout set — defaults to 10 seconds, which is too short for many real APIs. Set explicitly up to 120s.
  • Parsing JSON with JSON.deserialize(body, MyClass.class) for unknown shapes — fails silently on unexpected fields. Use JSON.deserializeUntyped for resilience.
  • Calling out from a Future method when you need result back to the caller — Future is fire-and-forget; use Queueable for chains where you need the response.
  • Forgetting to handle 4xx and 5xx differently — 4xx means “you sent bad data, don’t retry”; 5xx means “server problem, do retry with backoff.”

How to answer in 30 seconds

“For outbound, set up a Named Credential with OAuth or API-key auth and use Http + HttpRequest in an Apex class — the callout:NamedCred prefix injects auth. Never callout from a trigger; enqueue a Queueable with Database.AllowsCallouts. For inbound, use a Connected App + OAuth and either standard sObject REST or a custom @RestResource Apex class. Use idempotency keys, set a timeout, and switch to Platform Events for high-volume async patterns.”

How to answer in 2 minutes

Walk through outbound architecture (Named Credential → Apex callout → idempotency key → timeout → callout-not-allowed-in-trigger rule → Queueable wrapping), then inbound (Connected App + OAuth, standard REST vs custom Apex REST with upsert on External ID for idempotency), then the async tier (Platform Events, CDC, Pub/Sub API). Close on the four interview anti-patterns: hard-coded tokens, sync callouts in triggers, missing idempotency, missing timeouts.

Likely follow-up questions

  • How does OAuth 2.0 differ from API-key auth in Named Credentials?
  • What’s the maximum callout timeout?
  • Why can’t you call out from a trigger directly?
  • What’s the difference between Platform Events and Change Data Capture?
  • How would you handle a 429 rate-limit response from the external API?

Verified against: Apex Developer Guide — Callouts, Named Credentials Overview, Apex REST, Platform Events Developer Guide. Last reviewed 2026-05-19.