Skip to contentIntroducing the Integration Factory
Back to Blog
Integration

How to Integrate Salesforce With an ERP System

Mar 19, 2026
ATAppnigma Team
How to Integrate Salesforce With an ERP System

Most Salesforce ERP integrations are a short list of flows, not one big sync. Closed deals go out as orders, and customers, invoices and stock levels come back in. Each flow needs its own method. Apex callouts handle requests Salesforce starts, events or Change Data Capture push changes out, and the ERP writes back through the REST or Bulk API. The decision that matters most is which side starts each flow, because that decides which Salesforce limits you run into.

For a SaaS company the question is usually harder than for an IT team. Your customers run different ERPs, and your connector has to install into every customer's org and keep working after their admins change things. Appnigma builds these for SaaS teams. Its done for you service says "Build your product. We'll build the integrations." The page promises support for "each customer's objects, field mappings, and workflows." If you are asking whether Salesforce itself can act as an ERP, Salesforce's What Is ERP explainer covers that question.

Updated October 2026. Checked against the Apex Developer Guide, the Platform Events Developer Guide, the Named Credentials guide and Salesforce's API limits reference. The earlier version gave the daily API limit as 1,000 calls per user capped at one million, said platform events are processed exactly once, and showed a platform event trigger making the ERP callout. Salesforce documents all three differently. This update also adds Salesforce Connect, idempotent writes, and how packaged credentials work for customers on different ERP instances.

Five ways to connect Salesforce to an ERP

Each method fits one direction of data flow, so most production integrations combine two or three of them. Salesforce's own Integration Patterns and Practices guide names the same building blocks, and its Batch Data Synchronization pattern mentions ERP by name, suggesting you "run change data capture against ERP and Salesforce data sets."

MethodWho starts the flowGood forWhat limits it
Apex callout through a named credentialSalesforcePrice, stock or credit checks while a rep works a deal100 callouts and 120 seconds per transaction, no callouts from triggers
Platform events or Change Data CaptureSalesforce publishes, a subscriber actsSending won deals, account changes and orders to the ERPAt-least-once delivery, 72 hour retention
ERP writes through the REST or Bulk APIThe ERP or middlewareInvoices, payments and stock levels coming backThe customer org's daily API allocation
Salesforce Connect external objectsSalesforce reads on demandShowing ERP orders or invoices without copying themOData adapter or an add-on license for custom adapters
Middleware such as MuleSoft or BoomiThe integration platformMany systems, on-premises SAP, a team that runs a hubLicence cost and a separate platform to operate

Salesforce Connect is the method most guides skip, and it suits read-heavy ERP data well. Salesforce's Data Virtualization pattern describes it as a way to "pull data from legacy systems such as SAP, Microsoft, and Oracle in real time without making a copy." It works when the ERP exposes OData 2.0 or 4.0. A custom adapter built with the Apex Connector Framework needs a Salesforce Connect add-on license, which matters if you want to ship it to customers.

Middleware is the better choice in some cases, and it is worth saying when. MuleSoft's SAP Connector is "based on SAP NetWeaver" and handles BAPIs and IDocs, which an Apex callout can't speak to directly. If your customers run on-premises SAP, or your own IT team already runs an integration hub, a platform built for that is cheaper than rebuilding it in Apex. For comparing those platforms, see our guide to Salesforce integration tools for your own org.

The Apex limits that decide your sync design

Callout limits are where ERP integrations usually fail first, because a design that works for one record breaks on a bulk update. Salesforce's callout limits page sets the numbers below, and the governor limits table adds the size and queue limits.

LimitValueWhat it means for an ERP sync
Callouts per transaction100One callout per record stops working past 100 records
Timeout per callout10 seconds by default, 120 seconds at mostSlow ERP endpoints need an explicit setTimeout
Total callout time per transaction120 secondsTen 12 second ERP calls use up the whole transaction
Callout request or response size6 MB synchronous, 12 MB asynchronousLarge ERP exports belong in paged requests or files
Callouts after DMLNot allowed in the same transactionMake every callout first, then write the results
Queueable jobs from one transaction50 synchronous, 1 from an async jobA queueable can chain only one follow-up job

Two of these rows trip up experienced developers. The timeout row is cumulative, so the page says "the maximum cumulative timeout for callouts by a single Apex transaction is 120 seconds," which is not a per-request allowance. The DML row is the cause of the "You have uncommitted work pending" error. The same page says "you can't make a callout when there are pending operations in the same transaction," so log the result after the callout, never before it.

Limit exceptions also can't be caught. The governor limits page says a limit exception "can't be handled," so a design that hopes to catch the 101st callout and retry will instead roll back the whole transaction with an error the user sees.

Why can't a Salesforce trigger call the ERP directly?

An Apex trigger can't make a callout in its own transaction, and that includes platform event triggers. The triggers page says "callouts must be made asynchronously from a trigger," and the platform event subscriber page says event triggers "generally can't make Apex callouts." The supported pattern is a trigger that collects record IDs and hands them to a queueable job that implements Database.AllowsCallouts, per the Queueable Apex page.

This example sends won opportunities to an ERP as orders. It makes all callouts before any DML, stays under the callout limit, and chains one follow-up job for the rest. We haven't run this exact code, so test it in a sandbox against your ERP's endpoint first.

trigger OpportunityErpSync on Opportunity (after update) {
  List<Id> wonIds = new List<Id>();
  for (Opportunity opp : Trigger.new) {
    if (opp.IsWon && !Trigger.oldMap.get(opp.Id).IsWon) {
      wonIds.add(opp.Id);
    }
  }
  if (!wonIds.isEmpty()) {
    System.enqueueJob(new ErpOrderSync(wonIds));
  }
}
public class ErpOrderSync implements Queueable, Database.AllowsCallouts {
  private static final Integer MAX_CALLOUTS = 90;
  private final List<Id> oppIds;
  public ErpOrderSync(List<Id> oppIds) {
    this.oppIds = oppIds;
  }
  public void execute(QueueableContext ctx) {
    List<Id> batch = new List<Id>();
    List<Id> remaining = new List<Id>();
    for (Id oppId : oppIds) {
      if (batch.size() < MAX_CALLOUTS) {
        batch.add(oppId);
      }
      else {
        remaining.add(oppId);
      }
    }
    List<Erp_Sync_Log__c> logs = new List<Erp_Sync_Log__c>();
    for (Opportunity opp : [SELECT Id, Amount, Account.ERP_Customer_Id__c
      FROM Opportunity WHERE Id IN :batch]) {
    HttpRequest req = new HttpRequest();
    // The named credential holds this customer's ERP URL and auth
    req.setEndpoint('callout:ERP_Orders/orders');
    req.setMethod('POST');
    req.setHeader('Content-Type', 'application/json');
    req.setTimeout(20000);
    // Send the Salesforce ID so the ERP can reject a duplicate order
    req.setBody(JSON.serialize(new Map<String, Object>{
      'externalReference' => opp.Id,
      'customerId' => opp.Account.ERP_Customer_Id__c,
      'amount' => opp.Amount
    }));
    Erp_Sync_Log__c log = new Erp_Sync_Log__c(Record_Id__c = opp.Id);
    try {
      HttpResponse res = new Http().send(req);
      log.Status_Code__c = res.getStatusCode();
      log.Response__c = res.getBody().left(32000);
    }
    catch (CalloutException e) {
      log.Status_Code__c = 0;
      log.Response__c = e.getMessage();
    }
    logs.add(log);
  }
  // DML only after every callout in this transaction
  insert logs;
  if (!remaining.isEmpty()) {
    System.enqueueJob(new ErpOrderSync(remaining));
  }
}
}

The numbers explain the 90 cap. A bulk update of 200 won deals makes one trigger call and one queued job. That job sends 90 orders and leaves room under the 100 callout limit for a token refresh, then chains one job for the next 90 and one more for the last 20. A platform event trigger can receive up to 2,000 events in one batch, per the trigger configuration page. That would mean a chain of 23 jobs, so either lower the trigger's batch size or send the ERP one request with many orders if its API accepts that.

If you need a scheduled sync instead, a Batch Apex class that implements Database.AllowsCallouts resets limits on every chunk. The default chunk is 200 records, per the Batch Apex page, so with one callout per record set the scope to 90 or fewer. A loop that stops at 90 while the scope stays at 200 silently skips the other 110 records in every chunk.

Events can arrive twice, so every ERP write needs a key

Platform events and Change Data Capture are the right way to push changes out of Salesforce, but they don't promise exactly-once delivery. Salesforce's platform events considerations describe "an at-least-once pub/sub model, not the exactly-once model," with "a small chance of duplicate events." Retries add more repeats. A trigger that throws EventBus.RetryableException runs "up to 10 times," and each retry resends the whole batch, per the retry page.

So the ERP side has to treat a second copy of an order as the same order. Send a stable Salesforce ID or event key with every write and have the ERP reject or merge a repeat. In the other direction, write ERP records into Salesforce with an upsert on an external ID field. Salesforce's sObject Collections upsert handles "up to 200 records based on an external ID field," so a retried batch updates the same rows instead of creating new ones.

Recovery needs its own plan. Salesforce keeps platform events and change events "for 72 hours," per the event durability page. If the ERP subscriber is down for a long weekend, replay can't reach back far enough, so keep a nightly reconciliation query that compares records changed since the last good sync. One more trap is timing. Publish Immediately is "the default option," and the event definition page warns that a subscriber "can sometimes receive the event message before the data is committed." For order events, choose Publish After Commit.

Shipping one ERP connector to many customer orgs

A SaaS company that sells into Salesforce customers usually ships the connector as a managed package, so every customer runs the same versioned code. The package format solves upgrades. It doesn't solve the three problems that come from each customer having a different ERP, a different login and a shared API budget. Our managed package guide covers building and versioning the package itself.

The first problem is credentials. You can package named credentials and external credentials, but per Salesforce's packaging page, "external credential certificates and access tokens aren't packageable." Each customer authenticates after install. The newer rule matters more for ERPs. Since February 2026, packaged named credentials "default to developer control." After install the customer "can't edit the named credential's settings, such as the URL." Salesforce lists the exceptions where you should switch to subscriber control, and both are ERP cases. One is a service where "every customer has a unique subdomain," which is how most cloud ERP accounts work. The other is an on-premises gateway with "a custom URL that you can't know at packaging time." Our post on named and external credentials for each customer's login walks through the setup. Legacy named credentials are "deprecated and will be discontinued in a future release," per the Named Credentials guide, so new connectors should use the external credential model.

The second problem is the API budget, because your connector spends the customer's allocation, not yours. Salesforce's API limits reference gives Enterprise Edition "100,000 + (number of licenses x calls per license type)" with 1,000 calls per license. The allocation covers the whole org rather than each user. It also says "API calls made from installed managed packages, count against your org limit." Here is the arithmetic for a 50 license Enterprise org. It gets 150,000 calls a day. An ERP that writes 20,000 stock updates one record per call uses about 13 percent of that. The rest is shared with every other app the customer runs. The same updates sent through sObject Collections in groups of 200 cost 100 calls.

The third problem is governor limits inside the customer's transactions. Per the governor limits page, certified managed packages "get their own set of limits for most per-transaction limits," while "namespaces in non-certified packages don't have their own separate governor limits." Before your package passes security review, its callouts share the 100 limit with the customer's own code in the same transaction. Our post on which governor limits a certified package gets on its own has the full list.

Upgrades follow the same line. A fix to your ERP mapping reaches existing customers only when they install the new version, unless you push it. Push upgrades are only for packages "that have passed the AgentExchange security review," per Salesforce's push upgrade page, and they need a Partner Support case to turn on. Plan support for customers who stay on an older version for months. For SaaS teams that want the package built as a product, Appnigma's marketplace apps work delivers "a Salesforce managed package or HubSpot app built around your product and customer workflows."

Mistakes that break ERP integrations after go-live

Most ERP integration failures pass testing with a handful of records and only show up under real volume, retries or a second customer org. These are the seven that Salesforce's documented limits make predictable.

  1. One callout per record in a trigger path. It fails as soon as someone mass updates more than 100 records, and a limit exception can't be caught. Collect IDs and hand them to a queueable or batch job.
  2. Writing the log before the callout. Any insert before the callout causes the uncommitted work error. Make every callout, then write every log row.
  3. Inserting ERP records instead of upserting them. A retried event or a replayed batch then creates duplicate accounts or invoices. Upsert on an external ID.
  4. Relying on replay for recovery. Events are kept for 72 hours, so an outage longer than that needs a reconciliation job, not a replay.
  5. One endpoint URL baked into the package. Under developer control the customer can't change it. Use subscriber control for per-customer ERP hosts.
  6. No error log object. When the ERP rejects a record, nobody sees it until a rep notices stale data. A custom object with the record ID, status code and response body makes every failure searchable in reports.
  7. Assuming the API budget is yours. Each customer org shares its allocation across every installed app, so batch your writes and show customers how many calls your connector uses.

Frequently Asked Questions

What is the best way to integrate Salesforce with an ERP?

Use a different method per flow. Apex callouts suit checks a rep starts, platform events or Change Data Capture suit pushing deals and orders out, and the ERP's writes back should be upserts through the REST or Bulk API.

Can a Salesforce trigger call an ERP API?

Not in the same transaction. Salesforce requires callouts from triggers, including platform event triggers, to run asynchronously, so the trigger queues a job that implements Database.AllowsCallouts.

How many API calls does an ERP integration use in Salesforce?

It depends on how you batch. Enterprise Edition orgs get 100,000 calls a day plus 1,000 per license, shared across all apps. sObject Collections lets one call write up to 200 records.

Do I need MuleSoft to connect Salesforce to SAP?

Not always. Cloud ERPs with REST or OData APIs work with Apex callouts or Salesforce Connect, while on-premises SAP that speaks BAPIs and IDocs is where a connector like MuleSoft's SAP Connector earns its separate licence.

Can Appnigma build our Salesforce ERP connector?

Yes. Appnigma's done for you service builds customer integrations with ongoing maintenance, and its marketplace apps work packages them as a managed package. Its pages say Salesforce and HubSpot are the core specialties and other systems "can also be scoped," so the ERP side is scoped per project.

About the author

Sunny Chauhan is the co-founder and CEO of Appnigma AI, which builds and maintains customer integrations and native Salesforce and HubSpot apps for B2B SaaS teams. Before Appnigma, he was on Salesforce's AppExchange team, where he worked with more than 300 SaaS partners. He has spent close to a decade on the Salesforce platform, including work at Zennify, and holds Salesforce certifications including Platform Developer II, Platform App Builder, Administrator, Data Cloud Consultant, and AI Associate.

Sources

  1. Integration Patterns and Practices summary, Batch Data Synchronization and Data Virtualization
  2. Callout Limits and Limitations, Apex Developer Guide
  3. Execution Governors and Limits, Apex Developer Guide
  4. Triggers, Queueable Apex and Using Batch Apex, Apex Developer Guide
  5. Subscribe to Platform Event Messages with Triggers, Configure the User and Batch Size and Retry Event Triggers
  6. Platform Event Considerations and Define a Platform Event, Platform Events Developer Guide
  7. Event Message Durability, Pub/Sub API Developer Guide
  8. Upsert Records Using sObject Collections, REST API Developer Guide
  9. Package Named Credentials and Get Started with Named Credentials
  10. API Request Limits and Allocations, Salesforce Developer Limits and Allocations Quick Reference
  11. Push a Package Upgrade for 2GP, Second-Generation Managed Packaging Developer Guide
  12. Apex Connector Framework, Apex Developer Guide
  13. SAP Connector, MuleSoft Documentation

All Blogs

Bring the customer requirements and your current setup. Get a clear scope and delivery timeline to take back to them.