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:
| Need | Tool |
|---|---|
| Salesforce → external, fire-and-forget | Platform Event published by Salesforce, external subscribes via CometD / Pub/Sub API |
| External → Salesforce, fire-and-forget | External publishes Platform Event via REST, Salesforce subscribes via Trigger on the event |
| Stream all Salesforce data changes | Change Data Capture (CDC) — Salesforce publishes change events automatically |
| Bulk one-time data sync | Bulk 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. UseJSON.deserializeUntypedfor 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.