Inbound Apex REST and SOAP
Sometimes the standard Salesforce REST/SOAP APIs (covered in Salesforce APIs) don't match what an external system expects — maybe it needs a custom payload shape, or needs to trigger multi-object business logic in one call. That's what Apex REST and Apex SOAP web services are for: you write the endpoint, Salesforce exposes it.
Apex REST
Annotate a class with @RestResource and its methods with the HTTP verb they handle:
@RestResource(urlMapping='/OrderIntake/*')
global with sharing class OrderIntakeService {
@HttpPost
global static ResponseWrapper createOrder() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
Map<String, Object> payload =
(Map<String, Object>) JSON.deserializeUntyped(req.requestBody.toString());
try {
Order__c order = new Order__c(
External_Id__c = (String) payload.get('externalId'),
Amount__c = (Decimal) payload.get('amount')
);
insert order;
res.statusCode = 201;
return new ResponseWrapper(order.Id, 'created');
} catch (Exception e) {
res.statusCode = 400;
return new ResponseWrapper(null, 'error: ' + e.getMessage());
}
}
global class ResponseWrapper {
global Id recordId;
global String status;
ResponseWrapper(Id recordId, String status) {
this.recordId = recordId;
this.status = status;
}
}
}
This becomes reachable at:
POST /services/apexrest/OrderIntake/
Authorization: Bearer <access_token>
Content-Type: application/json
{ "externalId": "EXT-4821", "amount": 199.99 }
Apex SOAP web services
For SOAP, annotate a class global with WebService methods. Salesforce
auto-generates a WSDL you hand to the consuming system:
global class LegacyOrderService {
webservice static String createOrder(String externalId, Decimal amount) {
Order__c order = new Order__c(
External_Id__c = externalId,
Amount__c = amount
);
insert order;
return order.Id;
}
}
SOAP services are less common for new work — most teams reach for Apex REST unless an existing SOAP-only consumer requires it.
Request flow
Common mistakes
- Not validating input.
JSON.deserializeUntypedis permissive — check required fields exist and are the expected type before using them, and return a clear 4xx with a useful message when they aren't. - Returning 200 for everything, including failures. Use real HTTP status codes
(
201created,400bad request,404not found,500unexpected error) so callers can handle outcomes programmatically instead of parsing text. - Forgetting
with sharing. Without it, your Apex REST class can run in a context that bypasses the calling user's record-level sharing rules. - Skipping CRUD/FLS checks.
insert/updatestill respect field-level security only if your code checks it (or you use patterns likeSecurity.stripInaccessible); aglobal without sharingclass with no checks can let an under-permissioned caller write data they shouldn't. - Designing a bespoke endpoint when the standard REST API already does the job. Apex REST is for cases the standard API genuinely can't cover — custom validation, multi-object transactions, non-standard payload shapes — not a default.
Learn more on Trailhead
Apex Integration Services includes units on building and testing Apex web services.
Quiz
Check your understanding
Question 1 of 5What annotation exposes an Apex class as a custom REST endpoint?