- Integrating Salesforce well starts with deciding who is the source of truth: Salesforce, the internal application, or a shared model with explicit rules.
- For most internal applications, REST API with OAuth 2.0 and External IDs is enough. Bulk API 2.0 and events come in when there is volume, asynchronous latency or continuous synchronisation.
- The biggest mistake is not technical. It is mapping Salesforce objects as if they were normal tables and ignoring permissions, Record Types, picklists, automations and API limits.
- Do not make synchronous calls to Salesforce inside critical flows without a fallback. Treat Salesforce as an external system: slow, limited and occasionally unavailable.
- A good integration needs a queue, idempotency, auditing, reconciliation and observability from the start. Without that, users will be the ones who discover errors.
1. Before writing code, define the data contract
The question “how do I integrate Salesforce with an internal application?” sounds like an API question. In practice, it is a data architecture question.
Salesforce is rarely just a CRM database. It has validations, workflows, flows, Apex triggers, profile based permissions, calculated fields, layouts, Record Types, standard objects and custom objects. If your internal application treats Salesforce as a remote table called accounts, you will create a fragile integration.
The first step is to answer four questions.
First: who is the source of truth for each entity?
For example, an internal billing application may be the source of truth for plans, invoices and payment statuses. Salesforce may be the source of truth for accounts, opportunities and sales owners. But there are grey areas: tax address, VAT ID, segment, customer status, activation date. If these fields can be edited on both sides without a rule, you will get divergences.
Second: what is the direction of synchronisation?
There are one way integrations, such as “when a customer is created in the internal application, create or update Account and Contact in Salesforce”. There are two way integrations, such as “owner changes in Salesforce must be reflected in the internal application”. Two way sounds appealing, but it greatly increases complexity. You need conflict resolution, reliable timestamps, field level ownership and reconciliation.
Third: what latency is acceptable?
If the sales team needs to see an activation in Salesforce within the next 5 minutes, you do not need a synchronous integration. An asynchronous job is enough and more predictable. If a user cannot proceed through checkout without validating a status in Salesforce, you are putting an external dependency on the critical path. That should be a conscious decision, not an accident.
Fourth: what is the stable identifier?
Never rely only on the Name of an Account or the email of a Contact. Use External IDs in Salesforce. A field such as Internal_Customer_Id__c, marked as External ID and ideally Unique, allows you to upsert without first searching for the record. This reduces API calls and avoids duplicates.
At Impact Origin, when we design integrations of this type, we insist on this contract document before implementation. It does not need to be 40 pages long. But it must say, field by field, origin, destination, direction, requirement, transformation and conflict rule.
2. Choose the right integration pattern
There are four main patterns for integrating an internal application with Salesforce. The wrong choice usually shows up later as timeouts, operational costs or duplicate data.
Direct REST API
This is the most common option. The internal application calls the Salesforce REST API to create, update, query or delete records. The official documentation is at https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/.
It works well for moderate volumes, immediate operations and teams that want control over the code. In Node.js 22 LTS, Python 3.12.NET 8 or Java 21, the integration is straightforward. The critical point is not to spread Salesforce calls across the whole codebase. Create a dedicated module or service, with retries, logging, error normalisation and limits.
Pros: simple, full control, easy to test, no intermediary vendor.
Cons: you have to manage OAuth, rate limits, retries, pagination, idempotency and observability.
Bulk API 2.0
When you have tens or hundreds of thousands of records, REST is no longer the right tool. Bulk API 2.0 is asynchronous, works through jobs and is suitable for imports, migrations and large scale synchronisations. The official documentation is at https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/bulk_api_2_0.htm.
I would use Bulk API 2.0 to load 500,000 Accounts, update the statuses of 2 million subscriptions, or run nightly reconciliations. I would not use it for an interactive change made by a user.
Pros: built for high volumes, less pressure on individual REST calls, asynchronous model.
Cons: feedback is not immediate, error handling is per file or job, more operational complexity.
Platform Events and Change Data Capture
If you want to react to changes in Salesforce, Platform Events and Change Data Capture are good options. Instead of polling every minute, you subscribe to events. This reduces calls and improves data freshness. The documentation is at https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/ and https://developer.salesforce.com/docs/atlas.en-us.change_data_capture.meta/change_data_capture/.
I would use CDC to reflect Account, Contact or Opportunity changes in an internal application. But I would not assume this replaces reconciliation. Events can arrive out of order, can fail in the consumer, and there are retention windows. You need to store checkpoints.
Pros: good for continuous synchronisation, reduces polling, separates systems.
Cons: requires a persistent consumer, management of replay IDs, reconciliation and attention to retention limits.
iPaaS, such as MuleSoft, Workato, Make or Zapier
It can make sense when the team does not have technical capacity or when the integration is simple and not critical. For small internal workflows, it is acceptable. For core processes, with complex business rules, I have reservations. The problem is not the tool. It is critical business logic being hidden in visual flows that are difficult to version, test and review.
Pros: quick start, ready made connectors, useful for internal operations.
Cons: limited debugging, costs grow with volume, versioning and testing can be weak, risk of critical logic outside the main code.
My opinion: if the integration affects billing, onboarding, compliance or main operations, write it as part of your platform, with engineering discipline. If it is a side automation for a small team, an iPaaS may be enough.
3. Authentication: OAuth without dangerous shortcuts
Salesforce supports several OAuth 2.0 flows. The choice depends on the type of application.
For a server side internal application, the cleanest pattern is usually OAuth 2.0 JWT Bearer Flow or Web Server Flow with a refresh token stored securely. Avoid username password flow, even if it seems faster. It is documented, but it is a poor choice for production because it encourages storing human credentials and creates problems with MFA, rotation and auditing.
JWT Bearer Flow works well when you have a server-to-server integration. You configure a Connected App in Salesforce, associate a certificate, grant permissions and issue tokens without human intervention. The official documentation is at https://help.salesforce.com/ and in the OAuth section of the Salesforce platform.
Practical points you should not ignore:
- Create a dedicated technical user for the integration.
- Give minimum permissions, not a System Administrator profile out of laziness.
- Use specific Permission Sets for the required objects and fields.
- Store secrets in AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, Doppler or Vault. Not in loose variables without rotation.
- Record the
client_id,user,scope, expiry date and environment. - If you have sandbox and production, use separate Connected Apps and certificates.
A common trap: the integration works in sandbox with an admin user and fails in production with INSUFFICIENT_ACCESS_OR_READONLY. This happens because Field-Level Security and object permissions count in the API. The API is not a magic bypass for Salesforce rules. Test with the real technical user, not with your administrator user.
4. Model upserts, idempotency and errors
The most important operation in a Salesforce integration is the upsert. Always creating is dangerous. Always updating requires discovering the Salesforce ID. Upsert by External ID is the right middle ground.
Example with REST API, assuming API version v61.0. Confirm the supported version in your org in Setup or in the current documentation.
curl -X PATCH \
"https://your-instance.my.salesforce.com/services/data/v61.0/sobjects/Account/Internal_Customer_Id__c/cus_12345" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"Name": "ACME Ltd",
"BillingCountry": "PT",
"Customer_Status__c": "active"
}'
This pattern avoids a prior search by Internal_Customer_Id__c. If it exists, it updates. If it does not exist, it creates. For internal entities such as customers, subscriptions or operational units, this is almost always preferable.
But upsert does not solve everything. You need idempotency at your application level. If a job fails after sending the change to Salesforce but before marking the operation as completed, it will try again. That should be safe. External IDs help, but you should also store integration events.
A simple schema in PostgreSQL 16 may be enough:
create table integration_events (
id bigserial primary key,
provider text not null,
entity_type text not null,
entity_id text not null,
event_type text not null,
payload jsonb not null,
status text not null check (status in ('pending', 'processing', 'done', 'failed')),
attempts integer not null default 0,
last_error text,
created_at timestamptz not null default now(),
processed_at timestamptz
);
create index integration_events_pending_idx
on integration_events (provider, status, created_at);
This is not a complete messaging platform, but it gives auditability. In larger systems, I would use a dedicated queue such as SQS, RabbitMQ, Kafka, BullMQ over Redis, or pgmq over PostgreSQL, depending on the context. The point is simple: do not let the integration depend only on scattered HTTP calls.
You also have to classify errors.
400 errors due to field validation should not be retried indefinitely. They should go to dead letter with a clear message. 401 errors require token refresh or credential intervention. 403 errors indicate permissions. 429 errors or API limits require backoff and throughput reduction. 5xx errors require retry with jitter.
In terms of latency, design as if a REST call to Salesforce could take between 300 ms and 1200 ms at p95, depending on the network, internal automations and org load. Do not promise a p99 of 100 ms in a flow that depends on Salesforce. If you need an immediate response to the user, store locally, queue the synchronisation and show a “synchronising” status.
5. API limits, pagination and volume
Salesforce has limits. And those limits are not a detail. They are part of the architecture.
Daily API limits depend on the org edition, licences and add-ons. Do not assume a fixed number. Go to Setup, search for “System Overview” and confirm. You should also monitor the REST API limits endpoint, documented in Salesforce, through /services/data/vXX.X/limits.
In REST, a SOQL query can return paginated results. The first batch can bring up to 2000 records, and then you have to follow the nextRecordsUrl. Teams that ignore this end up with incomplete synchronisations and silent bugs.
There are also limits in Composite API. Composite API can group multiple subrequests, which reduces HTTP overhead, but it does not turn it into a universal transaction without costs. Confirm current limits in the official documentation before designing the batch. And pay attention to allOrNone: it can be useful, but it can also make an entire set fail because of an invalid field in a single record.
For volume, I like this practical rule:
- Up to a few hundred changes per day: simple REST with jobs is enough.
- Thousands to tens of thousands per day: REST with queue, batching and backoff.
- Hundreds of thousands or millions: Bulk API 2.0 and asynchronous reconciliation.
- Frequent changes originating in Salesforce: CDC or Platform Events.
Constant polling is the pattern I try to avoid. Querying Salesforce every 30 seconds to discover changes sounds simple, but it consumes API, misses events between windows if the query is poorly written, and creates operational pressure. If you really have to poll, use SystemModstamp fields, correct pagination, persisted checkpoints and overlapping windows to tolerate delays.
6. Data mapping: where integrations slowly die
The most underestimated part is field mapping.
Salesforce has standard objects such as Account, Contact, Lead, Opportunity, Case and Product2. But every real org has custom fields, validations, picklists and Record Types. The same field may be required for one Record Type and irrelevant for another. A picklist may accept Active in sandbox and Activo in production because someone manually changed values. This happens.
Common traps that appear in production:
- Picklists with different values between sandbox and production.
- Required fields imposed by validation rules, not by the visible schema.
- Flows that change fields after the API saves the record.
- Apex triggers that fail at higher volumes.
- Duplicate contacts because email was treated as a unique identifier.
- Poorly handled timezones in renewal dates, especially with
dateversusdatetime. - Currencies and countries stored as free text in an application and as a picklist in Salesforce.
- Sandboxes that are out of date compared with production.
The solution is to treat the mapping as a versioned contract. Store it in a repository. Review changes with the team that administers Salesforce. If possible, create contract tests that validate required fields, picklists and permissions against a sandbox.
I also recommend creating an internal “integration status” page. It should show the latest processed events, failures, summarised payload, Salesforce ID, External ID and a controlled retry button. This saves the support team hours and avoids engineering being called for every validation error.
Another important point: do not copy the entire Salesforce object to your database just because it is possible. Synchronise the fields your application needs. The larger the local mirror, the larger the surface for inconsistency.
7. Recommended implementation plan
A practical plan for integrating Salesforce with an internal application would be this.
First, do the inventory. List entities, fields, synchronisation direction, source of truth, expected volume, acceptable latency and conflict rules. Also include who can change each field in each system.
Second, prepare Salesforce. Create the Connected App, technical user, Permission Sets, External IDs and necessary fields. Confirm relevant Record Types, validation rules, flows and triggers. Do not leave this until the end.
Third, implement an isolated Salesforce client in the internal application. It should handle authentication, refresh or token issuance, retries, backoff, error normalisation and metrics. The rest of the application should not know REST API details.
Fourth, create the event or queue layer. Every relevant change should generate a persisted event. The worker processes it, calls Salesforce and updates the status. For low scale, a table in PostgreSQL may be enough. For larger scale, use a dedicated queue.
Fifth, implement reconciliation. Even with events, you need a periodic job that compares samples or complete sets. For example, active customers in the internal application that do not have Internal_Customer_Id__c in Salesforce, or Accounts in Salesforce with no local match. This detects silent failures.
Sixth, observe. Minimum metrics: pending events, failed events, average attempts, latency per call, error rate by HTTP code, API limits consumption, age of the oldest unprocessed event. Logs should include correlation ID, internal entity, Salesforce object, operation and normalised error. If you use OpenTelemetry, propagate trace IDs in the workers.
Seventh, roll out in phases. Start with low risk reading or writing. Then activate upserts for a subset of entities. Only then automate critical flows. Having a feature flag to turn off writing to Salesforce is a useful safety net.
My preference is to start simple, but not simplistic. REST API with OAuth, External IDs, a queue and reconciliation covers most cases. I would only introduce CDC, Platform Events or Bulk API when the volume and direction of the data justify it.
Conclusion
Integrating Salesforce with an internal application is not difficult because of the API. It is difficult because you are connecting two business models with different rules, owners and rhythms. Decide the source of truth, use External IDs, avoid unnecessary synchronous dependencies and build auditing from day one.
If you are facing a similar problem, book a call at https://impact-origin.com/agendamento.