Salesforce Custom Integration: Build One End to End
A Salesforce custom integration, end to end: the contract, named credentials, async callouts, the failure log most builds skip, and sandbox-first deploys.
A Salesforce custom integration is not hard because of the callout. The callout is nine lines. It’s hard because of everything that happens on the day the other system returns a 500 at 2:14 in the morning and nobody finds out until a rep asks why the invoice never showed up. Most integrations get built as a happy path and then live for three years as a black box that everyone is a little afraid of.
This is the build, in order, with the unglamorous parts in their proper place — which is first. We’ve covered when a custom integration beats a connector and which integrations are worth building at all. This one is the how.
Step 0: Write the contract before you write anything
Before Salesforce opens, answer four questions in plain sentences and put them somewhere permanent.
What triggers a sync? Not “when an Opportunity changes” — that’s a symptom of not having decided. “When an Opportunity moves to Closed Won and has at least one line item” is a decision.
Which system wins? If both sides can edit the same field, one of them is authoritative and the other is a mirror. Pick. Every two-way integration that has ever gone wrong went wrong here.
What is the identity key? The external record needs a home on the Salesforce record — an External ID field, unique, indexed. Not the Name. Not a formula. A real field you can upsert against.
What does “failed” mean? A 429 is retryable. A 422 is a bad payload and will never succeed no matter how many times you send it. If you can’t tell the two apart in advance, your retry logic will hammer a wall forever.
Four sentences. They take twenty minutes and they determine whether the next six hours produce something maintainable.
Step 1: Credentials go in Named Credentials, never in Apex
There is exactly one correct place for the endpoint and the secret, and it is a Named Credential paired with an External Credential. The External Credential holds the authentication — OAuth, an API key in a custom header, a JWT flow. The Named Credential holds the URL and points at it. Permission sets grant access to the principal.
Then your Apex references it by name:
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Billing_API/v2/invoices');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
No key in the code. No key in a Custom Setting somebody can query. The platform injects the authentication at callout time, and rotating the secret is a Setup change rather than a deploy.
This also means your sandbox can point at the vendor’s sandbox and production at production, with identical code. That sounds like a detail. It is the difference between testing your integration and hoping.
Step 2: The callout has to be asynchronous, and Salesforce will tell you so
Fire a callout from a trigger and you’ll meet this: You have uncommitted work pending. Please commit or rollback before calling out. Salesforce will not let you hold a database transaction open across a network call to somebody else’s server, which is correct of it.
So the trigger doesn’t call out. The trigger enqueues.
public class InvoiceSyncJob implements Queueable, Database.AllowsCallouts {
private Set<Id> oppIds;
public InvoiceSyncJob(Set<Id> oppIds) { this.oppIds = oppIds; }
public void execute(QueueableContext ctx) {
// query, build payload, call out, record the result
}
}
Use Queueable over @future. It takes real objects instead of primitives, it gives you a job ID you can monitor, and it chains. @future is a dead end you will want out of within a month.
One more thing the platform enforces: a transaction is capped at 100 callouts with a cumulative timeout of 120 seconds across all of them, per the callout limits documentation. If you sync 500 records in a batch, you are not making 500 callouts in one transaction. You are either batching the payload or chunking the job. Decide which now, not when the data volume triples.
Step 3: Field mapping is configuration, not code
The single most common reason an integration becomes untouchable is that the mapping between the two systems is buried in Apex, and changing which field the vendor’s customer_ref lands in requires a developer, a deploy, and a Tuesday.
Put the mapping in a Custom Metadata Type. One record per mapped field: Salesforce field API name, external field path, direction, required yes/no. Your Apex loops the metadata and builds the payload from it.
This buys you three things. Admins can add a mapped field without code. The mapping deploys with the integration as metadata, so environments can’t drift. And custom metadata queries don’t consume your SOQL limits, so you can read the config in a loop without paying for it.
The code gets shorter, too. A mapping-driven serializer is about forty lines and never changes again.
Step 4: Build the failure log before the happy path
Here is the step everyone skips and everyone regrets.
Create a custom object — Integration_Event__c or whatever your naming convention is. Fields: related record, direction, status, request payload, response body, HTTP status code, attempt number, timestamp, and an error message. Write a row on every attempt, success and failure alike.
That object is the entire difference between an integration you operate and an integration you fear. When a rep asks why the invoice never showed up, the answer is a report filter, not a developer opening logs. When the vendor claims they never received it, you have the payload and the timestamp.
Two practical notes. Truncate payloads before storing them — a long text area is 131,072 characters and a verbose API will exceed that. And never log the auth header; if you’re storing the full request, strip it explicitly.
Build this object first, before the callout works. It’s the harness you’ll debug the rest of the build inside of.
Step 5: Retries that back off instead of stampeding
Your contract from Step 0 already sorted retryable from terminal. Now act on it.
On a retryable failure — timeouts, 429, 5xx — re-enqueue with an increasing delay and a hard cap. Three or four attempts, spaced out, then stop and mark the record as needing attention. Salesforce lets you delay a Queueable by minutes when you enqueue it, which is enough to implement real backoff without a scheduled job.
On a terminal failure — 400, 422, an auth rejection — do not retry. Mark it failed, write the response body to the log, and surface it. A bad payload retried forty times is forty identical rows and one very annoyed API partner.
The rule: retries are for the other system having a bad minute, not for your payload being wrong.
Step 6: The inbound direction, if you need one
Outbound is half the job. If the other system needs to push updates back, expose an Apex REST endpoint:
@RestResource(urlMapping='/billing/invoice/*')
global with sharing class InvoiceInboundApi {
@HttpPost
global static void receive() { /* parse, validate, upsert on External ID */ }
}
Three rules for inbound. Validate before you write — an inbound endpoint is an unauthenticated-looking door into your data model, and the payload is whatever the other side felt like sending. Upsert on the External ID from Step 0, never on Name matching. And log every inbound call to the same Integration_Event__c object, so one report shows both directions.
If near-real-time isn’t required, a scheduled pull is simpler and fails more gracefully than an endpoint you have to keep available.
Step 7: Tests, and the mock you can’t skip
Apex tests can’t make real callouts. You implement HttpCalloutMock and register it with Test.setMock(), which means your tests are only as honest as your mocks.
So mock the failures, not just the success. A 200 with the expected body proves nothing interesting. Write a test for the 500 that should retry, the 422 that shouldn’t, the timeout, and the malformed JSON — and assert on what landed in Integration_Event__c. That assertion is the one that catches the regression six months from now.
Production deploys require 75% org-wide coverage, but coverage is the floor, not the goal. Four honest failure tests are worth more than a hundred lines of getters exercised for the percentage.
Step 8: Deploy sandbox-first, against the vendor’s sandbox
Named Credentials made this cheap: same code, different endpoint. Deploy to a sandbox, point it at the vendor’s test environment, run real records through it, and read the Integration_Event__c rows. You are looking for the payload the other system actually accepted, not the one your mock said it would.
Then promote to production with tests, and watch the log object for the first day. Most integrations that break in production break in the first twenty-four hours, and almost always on a data shape nobody had in the sandbox — the account with no billing address, the opportunity with a line item quantity of zero.
What this looks like when an AI builds it
Every step above is well-defined work against documented APIs, which is exactly the kind of work an AI does well when it has real access to your org rather than a chat window and a guess. Point it at a sandbox, describe the contract from Step 0, and it can read your existing schema, build the metadata type, write the Queueable, generate the failure-test suite, and deploy — while you review, which is where your time is actually worth something.
The honest objection is trust, and the answer isn’t that the AI won’t get something wrong. It’s that the work is visible and recoverable: changes are logged with who and when, a snapshot is taken before the deploy, and Salesforce deploys go to a sandbox with tests first. That’s the safety layer in a sentence, and there’s a deep dive if you want it. Pricing is flat per Sentinel and is covered on a short demo call.
If you’ve never given an AI real access to an org, start read-only — connecting Claude to your Salesforce org takes an afternoon and lets it describe your current integrations back to you before it writes anything. Write access is a separate, deliberate step, and it should be.
Start with the one field
Don’t build the whole integration. Build one field, one direction, with the log object and one failure test.
Pick the field somebody currently retypes between two browser tabs. Sync that, end to end, with retries and logging. It’ll take a day and it will teach you every wrong assumption in your contract while the blast radius is one field — which is the same reason Flow is the right tool right up until it isn’t, and the reason the second integration always takes a third of the time the first one did.
Then add the second field. The scaffolding is already there.
Book a Demo Call and bring the integration your team is currently doing by hand. That’s the one worth scoping.
KEEP READING
Salesforce MCP Write Access: Set It Up Safely
Salesforce MCP write access is real — but it writes records, not your org. How to enable it safely, and exactly where the ceiling sits.
Salesforce Renewal Alerts Automation Nobody Ignores
Salesforce renewal alerts automation usually ships as a report subscription nobody reads. Build the escalating version instead — no developer required.
Ready to see what AI can do for your business?
Start a Conversation