Skip to content

Custom Apex REST

Module 5 of 7 in the chapter Salesforce as an API Provider, chapter 8 of the Integration Architect course. Each module stands on its own. Previous: GraphQL and the UI API · Next: When Someone Calls You Back

The question this module answers: When should a caller use Salesforce’s standard API, and when should you build your own door with a custom Apex REST service?

On the map: Your own doors On the map: Your own doors. One row from the map of every door into Salesforce, in module 1: the set your own doors, with its doors: Apex REST, taught here, and Apex SOAP, chapter 17. {"author": "Steve Simpson", "blueprint": "1.0.1", "builder": "standard 1.58.0", "built": "2026-11-03", "id": "DGM-INT-SP-12", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "cells 0.5.0", "theme": "stevetecharc 1.4.0"} THE SET: WHAT THE CALLER IS DOING THE DOORS Your own doors you write them, you version them Apex REST your contract · ch 8.5, 8.6 Apex SOAP your WSDL · ch 16 One set from the map of every door into Salesforce (module 1). HOW TO READ IT taught in this chapter taught in another chapter © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

Where this module sits on the map.

Every door so far has been Salesforce’s: REST, composite, GraphQL, the UI API. The caller learns your org and uses Salesforce’s standard calls against it. There’s another choice. You write the door yourself, in Apex, with a contract you design: send me this, and I’ll answer with that.

This is one of the first big decisions on an integration project. It decides how much the other team has to know about your org, and who fixes what when it breaks.

The Standard Door: They Need All the Details

Section titled “The Standard Door: They Need All the Details”

Take the team building the integration layer for Redrock. If they call Salesforce’s standard API, they need to know all the details. What are your objects? What are your fields? In what order do they write them, a manifest before its Work Orders before their line items? They work almost as if they were Apex developers inside your org: they follow all your rules, and they handle the timeouts.

That has real advantages. Nobody on your side writes code. The standard connectors in every integration tool already speak Salesforce’s API. And the responsibility sits with them: they write the calls, so they own the errors.

But it ties them to your inner workings. Rename a field, split an object, change a picklist value, and their integration breaks. Change one field, and the integrations break. So they end up in your design meetings, because every change you make is a change they have to make.

With a custom Apex REST service, they don’t need the details. You write a contract: call this address, pass in this JSON, get back this response. Something like “save manifest” and “get delivery”. You can even hide the __c from them. Behind the door, your Apex does the work in the right order, against your objects.

Then you can move your Salesforce org around as much as you like. As long as you keep the contract, they’re fine.

On one project, that’s exactly why we built our own doors. There was a lot of complexity in what the org did with each incoming call, and we wanted to hide it from the middleware team. And because every call came through our own code, we could record it. I could time-stamp every incoming call. I knew when it happened. I knew how long it took. I even recorded the payload. We wrote that log asynchronously, so it didn’t slow the call down. And because the callers passed a unique ID on every request, during load testing I could match our log line for line with the performance test results. Chapter 14 designs that log properly: what to keep, for how long, and what not to keep.

The price is that it’s now your code. You need good Apex developers, you test it, and you own its errors.

Standard door or your own door Standard door or your own door. Two rows, both starting from the integration layer for Redrock. Top row: it calls Salesforce's standard REST API directly on the Manifest, Work Order and Work Order Line Item objects; it needs to know every object, field and rule, and a renamed field breaks it. Bottom row: it calls Steve Trucking's own door, manifests/v1, a custom Apex REST service; it needs only the contract, the request and the result, which are light Apex classes of their own. Behind the door, your Apex maps them onto the same three objects, writes them in the right order, and records every call in the integration log. Underneath: the standard door puts the knowledge, and the errors, with the caller; your own door keeps them with you. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.57.1", "built": "2026-10-31", "id": "DGM-INT-SP-04", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "cells 0.5.0", "theme": "stevetecharc 1.4.0"} REDROCK'S SIDE THE DOOR INSIDE STEVE TRUCKING'S ORG The integration layer knows every object, field and rule Salesforce's standard REST API a renamed field breaks the caller standard door Manifest, Work Orders, line items written by the caller, in the caller's order The integration layer knows only the contract: the request and the result Your own door: manifests/v1 custom Apex REST with its own request and result classes; you version it your own door Manifest, Work Orders, line items written by your Apex, in the right order The integration log every call: when, how long, the payload calls reads, writes save manifest matches, upserts records The standard door puts the knowledge, and the errors, with the caller. Your own door keeps them with you. © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

Who has to know your org: the caller, or only your code.

There’s no single right answer. I’ve used both. The question is whether the other team should know your inner workings or not.

Standard APIYour own door
The caller needs to knowEvery object, field and ruleThe contract
A field changes in your orgThe caller changes tooOnly your Apex changes
Who writes the codeThe callerYou
Who owns the errorsMostly the callerMostly you
A log of every callFrom Salesforce’s event logsYour own, as detailed as you design it
Salesforce’s version retirementsApply to the callerDon’t apply: you version it

If the integration is simple, just a read or just a write, I’d lean towards the standard API. If there’s a lot of processing behind each call, or several teams depend on the shape of your data staying put, I’d build the door. Steve Trucking chooses its own door for Redrock’s manifests, because what happens to a manifest inside the org (matching, routing, scheduling) will keep changing, and Redrock shouldn’t have to.

In Apex, a class becomes a door with one annotation, @RestResource, which gives the door its address. Each method answers one verb, @HttpGet or @HttpPost, and the class and methods are global (Introduction to Apex REST (opens in a new tab), Apex Developer Guide, checked 2026-10-31). The caller reaches it at /services/apexrest/ plus your address, with the same token and integration user as any other door.

When I teach this, I build the door up in five steps, each one a little more than the last:

  1. A GET that returns a list of sObjects. Query the accounts and return them. It works, and the caller gets Salesforce’s standard JSON. But if all you need is a simple query, you don’t need a custom door at all; the standard API already does that.
  2. A GET that returns a response class. Instead of the sObjects, return a light Apex class: a customer with just the ID and the name. Now I control exactly what goes back to the client. That response could be a contract I build with the external team.
  3. A GET with a parameter. Read the parameters from RestContext.request.params, a map, and use one in the query’s WHERE clause. Now the caller asks for one customer.
  4. A POST with a request class and a response class. The caller sends a customer in; my code turns the JSON into a request class with one line, creates the account, and answers with a response class: a status, and a list of errors if there are any.
  5. A POST that returns an error. The same as step 4, with the failure caught and the HTTP status code set on the response, so the caller can tell from the status alone that it failed.

Steps 4 and 5 are the door you actually build. Here’s step 4, from my own demo code:

@RestResource(urlMapping='/CustomWS4/*')
global with sharing class STA_CustomWS_4 {
@HttpPost
global static STA_CreateCustomerResponse createCustomer() {
STA_CreateCustomerResponse resp = new STA_CreateCustomerResponse();
// Read the incoming request payload
String payload = RestContext.request.requestBody != null
? RestContext.request.requestBody.toString() : '';
// Parse it into an Apex object
STA_CreateCustomerRequest req = STA_CreateCustomerRequest.parse(payload);
Account newAcct = new Account();
newAcct.Name = req.Customer.Name;
try {
insert newAcct;
resp.status = 'success';
} catch (Exception ex) {
resp.status = 'error';
resp.errorList = new List<String>{ ex.getMessage() };
}
return resp;
}
}

Look at what goes in and what comes out. STA_CreateCustomerRequest in, STA_CreateCustomerResponse out. Neither one is an Account.

Those two classes are the contract. They’re small classes defined in Apex that represent only the structures coming in and the structures going out, and nothing else.

Apex REST will let you put sObjects straight into the contract, an Account in and an Account out. Step 1 did exactly that. Don’t build on it. The moment an sObject is in the contract, your data model is in the contract. The caller sees your field names, __c and all. Every field you add or rename changes what they send and receive. And a caller can send fields you never meant to accept, like an owner or a status, and your code has to remember to ignore them. That’s the standard door’s problem, back again through your own door.

For Steve Trucking’s manifest door, the contract is four light classes:

// What comes in
global class ManifestRequest {
global String manifestKey; // REDROCK-20261031
global Date deliveryDate;
global List<DeliveryIn> deliveries;
global static ManifestRequest parse(String json) {
return (ManifestRequest) System.JSON.deserialize(json, ManifestRequest.class);
}
}
global class DeliveryIn {
global String deliveryKey; // REDROCK-8472914-1
global String distributionCenter;
global String recipientName;
global List<String> items;
}
// What goes out
global class ManifestResult {
global String manifestKey;
global Integer accepted;
global List<DeliveryResult> deliveries;
}
global class DeliveryResult {
global String deliveryKey;
global String status; // ACCEPTED or REJECTED
global String problemType; // set only when rejected
}

That parse method is one line, and it’s the approach from my blog post JSON Jamboree: Fixed vs. Dynamic Deserialization: when the JSON’s structure is straightforward and doesn’t change often, write an Apex class that matches it exactly and deserialize it in one line. A contract is exactly that kind of structure: one you’ve agreed not to change. (The post shows the other two approaches, a hand-written parser and mappings kept in custom metadata, for the times you’re reading someone else’s JSON that won’t hold still.)

The door itself, /services/apexrest/manifests/v1/, reads the body, calls ManifestRequest.parse, and maps the request onto your objects: deliveryKey becomes Partner_Delivery_Key__c, distributionCenter is looked up and becomes a reference, items become Work Order Line Items. Then it builds the result. The caller never sees those names. When you change your data model, you change the mapping, not the contract.

Salesforce’s guide gives you a second way to take the request: declare the class as the method’s parameter, and Apex REST “deserializes request data into” it for you (Apex REST Methods (opens in a new tab), checked 2026-10-31). It’s shorter, but the parameter’s name becomes part of the body: a parameter called manifest means the caller sends { "manifest": { ... } }. Reading the body yourself and parsing it, as in my demo, keeps the body’s shape entirely yours. Either way, two rules from the same page hold:

  • Only the types Apex REST allows: primitives, lists, maps with String keys, and your own classes made of them.
  • No circles. If two of your classes point at each other, Salesforce answers the call with a 400 at run time. A contract is a tree: a manifest holds deliveries, a delivery holds items.

The classes also make the versions clean. v1’s classes are v1’s contract. When v2 needs a different shape, it gets its own classes, and v1’s stay exactly as they were until v1 retires.

  • The version is in your address. Salesforce doesn’t version this door; its version retirements “do not impact” custom Apex REST services (module 1). So you put v1 in the address yourself, as chapter 3 taught, and when you need a breaking change you publish v2 beside it.
  • It still counts. “Calls to Apex REST classes count against the organization’s API governor limits. All standard Apex governor limits apply” (Introduction to Apex REST (opens in a new tab)). Your door is still one door into the same building.
  • Whose access does it run with? Declare the class with sharing, as my demo does, and query and save in user mode, as Salesforce’s own sample does (WITH USER_MODE, insert as user; Apex REST Basic Code Sample (opens in a new tab), checked 2026-10-31). Then the code sees and changes only what the integration user may. Apex can run with more access than the caller has, and your door shouldn’t hand that access out.
  • Who can reach the door at all. Salesforce has a separate permission for this, Apex REST Services, which “enables access to Apex REST services even if API Enabled permission is not granted” (Which API Do I Use? (opens in a new tab), Salesforce Help, checked 2026-10-31). So a user who can’t call Salesforce’s standard API can still be allowed through your door. Give it on purpose, to the users who need your door and no other.
  • The contract is written down. The request and response classes are the contract in code; publish it the way chapter 3 showed, so the caller doesn’t learn it by trial and error.

When the call fails, what do you send back? Salesforce’s own APIs answer errors in Salesforce’s own format. With your own door, you choose, and it’s worth choosing a standard instead of inventing one.

The web has one: Problem Details for HTTP APIs, RFC 9457, an IETF standard since July 2023. It defines “a ‘problem detail’ to carry machine-readable details of errors in HTTP response content to avoid the need to define new error response formats for HTTP APIs” (RFC 9457 (opens in a new tab), checked 2026-10-31). The answer has its own content type, application/problem+json, and a few standard fields:

{
"type": "https://steve-trucking.example/problems/unknown-distribution-center",
"title": "Unknown distribution center",
"status": 422,
"detail": "Delivery REDROCK-8472914-1 names distribution center PHX-9, which doesn't exist.",
"instance": "/services/apexrest/manifests/v1/REDROCK-20261031"
}

Step 5 of my demo set the status code on the response, and that’s the first rule: the HTTP status says it failed, not only a word in the body. Pick it by whose problem it is. If the caller sent something wrong, like a delivery with no distribution center, that’s a 4xx: the caller has to fix it, and sending it again won’t help. If your side failed, that’s a 5xx: the caller can try again later. A 500 for everything tells the caller to retry a request that will never work.

The second rule is the body. status is the HTTP status, the one from chapter 3. type says which kind of problem, so the caller’s code can act on it. detail says what happened, for the person reading the log. Most integration tools already understand the format, and your callers’ code doesn’t need a special case for your door.

One kind of error deserves its own answer: too many calls. If your door has to slow a caller down, the standard answer is 429 Too Many Requests, with a Retry-After header that says how many seconds to wait. A well-built caller waits that long instead of trying again at once and making it worse.

There’s also a newer way to warn callers before they hit the limit: RateLimit headers that tell the caller how much it has left. It’s still an IETF draft, not a standard (version 11, last updated 2026-05-23, datatracker (opens in a new tab), checked 2026-10-31). Watch it, and don’t build a contract on it yet.

In 2023 I made a video building five custom Apex REST services, from a simple GET to a POST that returns an error. The note under the video says what’s still right and what’s changed since.

STA 1.8 · Apex Web Service: Building Blocks for Web Wizards · 9:08 · published 2023-09-18 · plays from YouTube when you click it · Watch on YouTube · More videos: Salesforce Integration Architecture Videos
  • Standard door or your own. Does the other team need to know your inner workings? Simple read or write: standard. Complex processing, or a data model that will keep changing: your own.
  • Who owns errors. Standard: mostly the caller. Your own: mostly you, so plan the log (chapter 14).
  • The version in your address, and what happens to v1 when v2 arrives.
  • Whose access the code runs with. With sharing and user mode, unless there’s a written reason not to.
  • The contract’s own classes. Light Apex classes for what comes in and what goes out, never sObjects; the mapping onto your objects stays inside.
  • Your error format. Problem Details, with a type for each kind of problem your callers need to handle; 429 with Retry-After when they must slow down.