REST & SOAP APIs

0/3 in this phase0/41 across the roadmap

📖 Concept

Salesforce provides extensive API capabilities for both consuming (callouts from Salesforce) and exposing (custom APIs) data and functionality.

Inbound APIs (external systems calling Salesforce):

  • REST API — Standard CRUD on Salesforce records via HTTP (JSON/XML)
  • SOAP API — Enterprise-grade XML-based API
  • Bulk API 2.0 — For large data operations (millions of records)
  • Composite API — Multiple operations in a single request
  • Tooling API — Metadata operations (deployments, describe)
  • Streaming API — Push notifications (PushTopic, CDC, Platform Events)
  • GraphQL API — Query exactly the fields you need (newer)

Outbound (Salesforce calling external systems):

  • HTTP Callouts — Apex HttpRequest/HttpResponse
  • Named Credentials — Secure credential management
  • External Services — Low-code API integration
  • Outbound Messaging — Legacy SOAP-based notifications

Custom REST APIs in Apex: You can expose your own REST endpoints using @RestResource:

  • Custom URL: /services/apexrest/your-endpoint
  • Supports GET, POST, PUT, PATCH, DELETE
  • Full control over request/response format
  • Respects sharing and security settings

Governor limits for APIs:

  • 100 callouts per transaction
  • 120-second cumulative callout timeout
  • 6MB request/response size per callout
  • API calls count against daily API limits (based on edition)

💻 Code Example

codeTap to expand ⛶
1// REST & SOAP Integration
2
3// 1. Making HTTP Callouts from Apex
4public class ExternalApiService {
5
6 // GET request
7 public static String getExternalData(String endpoint) {
8 HttpRequest req = new HttpRequest();
9 req.setEndpoint(endpoint);
10 req.setMethod('GET');
11 req.setHeader('Content-Type', 'application/json');
12 req.setHeader('Accept', 'application/json');
13 req.setTimeout(30000); // 30 seconds
14
15 HttpResponse res = new Http().send(req);
16
17 if (res.getStatusCode() == 200) {
18 return res.getBody();
19 } else {
20 throw new CalloutException(
21 'API Error: ' + res.getStatusCode() + ' — ' + res.getBody()
22 );
23 }
24 }
25
26 // POST request with JSON body
27 public static Map<String, Object> createExternalRecord(Map<String, Object> data) {
28 HttpRequest req = new HttpRequest();
29 req.setEndpoint('callout:External_CRM/api/v1/customers');
30 req.setMethod('POST');
31 req.setHeader('Content-Type', 'application/json');
32 req.setBody(JSON.serialize(data));
33 req.setTimeout(60000);
34
35 HttpResponse res = new Http().send(req);
36
37 if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
38 return (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
39 } else {
40 throw new CalloutException('Create failed: ' + res.getBody());
41 }
42 }
43
44 // Using Named Credentials (best practice for auth)
45 public static String callWithNamedCredential() {
46 HttpRequest req = new HttpRequest();
47 // Named Credential handles authentication automatically
48 req.setEndpoint('callout:My_External_System/api/data');
49 req.setMethod('GET');
50
51 HttpResponse res = new Http().send(req);
52 return res.getBody();
53 }
54}
55
56// 2. Custom REST API (exposing Apex as API)
57@RestResource(urlMapping='/api/accounts/*')
58global with sharing class AccountRestApi {
59
60 @HttpGet
61 global static List<Account> getAccounts() {
62 RestRequest req = RestContext.request;
63 String searchTerm = req.params.get('search');
64
65 if (String.isNotBlank(searchTerm)) {
66 String search = '%' + searchTerm + '%';
67 return [
68 SELECT Id, Name, Industry, AnnualRevenue
69 FROM Account WHERE Name LIKE :search LIMIT 50
70 ];
71 }
72 return [SELECT Id, Name, Industry FROM Account LIMIT 100];
73 }
74
75 @HttpPost
76 global static Account createAccount() {
77 RestRequest req = RestContext.request;
78 Map<String, Object> body = (Map<String, Object>)
79 JSON.deserializeUntyped(req.requestBody.toString());
80
81 Account acc = new Account(
82 Name = (String) body.get('name'),
83 Industry = (String) body.get('industry')
84 );
85 insert acc;
86
87 RestContext.response.statusCode = 201;
88 return acc;
89 }
90
91 @HttpPut
92 global static Account updateAccount() {
93 RestRequest req = RestContext.request;
94 String accountId = req.requestURI.substringAfterLast('/');
95
96 Map<String, Object> body = (Map<String, Object>)
97 JSON.deserializeUntyped(req.requestBody.toString());
98
99 Account acc = new Account(Id = accountId);
100 if (body.containsKey('name')) acc.Name = (String) body.get('name');
101 if (body.containsKey('industry')) acc.Industry = (String) body.get('industry');
102 update acc;
103
104 return [SELECT Id, Name, Industry FROM Account WHERE Id = :accountId];
105 }
106
107 @HttpDelete
108 global static void deleteAccount() {
109 RestRequest req = RestContext.request;
110 String accountId = req.requestURI.substringAfterLast('/');
111 delete [SELECT Id FROM Account WHERE Id = :accountId];
112 RestContext.response.statusCode = 204;
113 }
114}
115
116// 3. Callout from Trigger (must use @future)
117public class TriggerCalloutService {
118
119 @future(callout=true)
120 public static void syncToErp(Set<Id> accountIds) {
121 List<Account> accounts = [
122 SELECT Id, Name, Industry, BillingCity
123 FROM Account WHERE Id IN :accountIds
124 ];
125
126 for (Account acc : accounts) {
127 HttpRequest req = new HttpRequest();
128 req.setEndpoint('callout:ERP_System/api/customers');
129 req.setMethod('POST');
130 req.setHeader('Content-Type', 'application/json');
131 req.setBody(JSON.serialize(new Map<String, Object>{
132 'sfId' => acc.Id,
133 'name' => acc.Name,
134 'industry' => acc.Industry
135 }));
136
137 try {
138 HttpResponse res = new Http().send(req);
139 if (res.getStatusCode() != 200) {
140 ErrorLogger.log('ERP sync failed for ' + acc.Id +
141 ': ' + res.getBody(), 'TriggerCalloutService', 'ERROR');
142 }
143 } catch (Exception e) {
144 ErrorLogger.log(e, 'TriggerCalloutService.syncToErp');
145 }
146 }
147 ErrorLogger.flush();
148 }
149}

🏋️ Practice Exercise

Integration Practice:

  1. Create a custom REST API that supports GET, POST, PUT, DELETE for a custom object
  2. Write an Apex class that makes a GET callout to a public API and parses the JSON response
  3. Set up a Named Credential for an external system and use it in a callout
  4. Implement a @future(callout=true) method called from a trigger to sync data externally
  5. Build a Bulk API integration that imports 50,000 records from a CSV
  6. Create a Composite API request that creates an Account and its Contacts in one API call
  7. Write a test class with HttpCalloutMock for all your callout methods
  8. Design an error handling and retry strategy for failed API callouts
  9. Implement rate limiting to stay within daily API call limits
  10. Build a real-time sync using Change Data Capture + External API callouts

⚠️ Common Mistakes

  • Making callouts from triggers without @future — synchronous callouts in triggers are not allowed. Use @future(callout=true) or Queueable with Database.AllowsCallouts

  • Not using Named Credentials — hard-coding credentials in Apex is a security vulnerability and fails security reviews

  • Ignoring callout governor limits — 100 callouts per transaction, 120-second total timeout. Plan for limits in bulk scenarios

  • Not writing HttpCalloutMock tests — callouts cannot be made in test context. Without mocks, your code has 0% test coverage for callout logic

  • Not handling HTTP error responses — checking only for 200 misses 201, 204, and other valid success codes. Check for ranges (200-299)

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for REST & SOAP APIs. Login to unlock this feature.