This comparison covers 12 major ERP systems across the spectrum of outbound event delivery capabilities, from native HTTP webhook support to event bus architectures to polling-only systems. The focus is on outbound notifications — what happens when a record changes inside the ERP and an external system needs to know about it.
This card does NOT cover inbound webhooks (external systems pushing data into the ERP). For deep-dive event architecture comparisons with protocol-level detail, see the ERP Event-Driven Integration Comparison card.
| Property | Value |
|---|---|
| Scope | 12 ERP systems (Salesforce, SAP S/4HANA, Oracle ERP Cloud, NetSuite, D365 F&O, D365 BC, Workday, IFS Cloud, Epicor Kinetic, Sage Intacct, Zoho, Acumatica) |
| Focus | Outbound event delivery — webhook, push, event bus, polling |
| Card Type | Cross-system comparison |
| Deployment Models | Cloud, hybrid, on-premise variants noted |
| Temporal Scope | 2024–2026 (current GA features only) |
Each ERP takes a different approach to outbound notifications. This table maps the primary outbound event mechanisms available on each platform. [src1, src2, src3, src4]
| ERP System | Native Outbound Mechanism | Mechanism Type | Protocol | Requires Add-on? | Latency (Typical) |
|---|---|---|---|---|---|
| Salesforce | Outbound Messages (SOAP) | HTTP POST callback | SOAP/XML | No | 1–5s |
| Salesforce | Platform Events / CDC | Event bus (pub/sub) | gRPC (Pub/Sub API) | High-volume license for >100K/day | <1s–5s |
| SAP S/4HANA Cloud | SAP Event Mesh | Event bus (pub/sub) | AMQP / Webhook (HTTP POST) | Yes (BTP subscription) | 1–30s |
| Oracle ERP Cloud | Business Events | Event bus | Oracle Integration Cloud | Yes (OIC subscription) | 5–30s |
| NetSuite | SuiteScript User Events | Custom script (HTTP POST) | REST/HTTPS | No (SuiteCloud Plus recommended) | 1–10s |
| D365 Finance & Operations | Business Events / Data Events | Event bus | Azure Service Bus / HTTPS | No (Azure endpoint needed) | 2–15s |
| D365 Business Central | Webhook Subscriptions | HTTP POST callback | REST/JSON | No | ~30s (batched) |
| Workday | None (polling only) | N/A | N/A | Middleware required | 1–15 min (poll) |
| IFS Cloud | Event Actions | Workflow-triggered HTTP POST | REST/HTTPS | No | 2–10s |
| Epicor Kinetic | None native (BPM workaround) | Custom code (REST call from BPM) | REST/HTTPS | No (requires customization) | 2–15s |
| Sage Intacct | Platform Services (limited) | Platform event triggers | REST/HTTPS | Custom Sender required | 5–30s |
| Zoho (Books/Inventory) | Workflow Webhooks | HTTP POST callback | REST/JSON | No | <5s |
| Acumatica | Push Notifications | HTTP POST callback | REST/JSON | No | 1–10s |
| ERP System | Daily Event Limit | Per-Hour / Burst Limit | Retry Policy | Notes |
|---|---|---|---|---|
| Salesforce (Outbound Msgs) | No hard daily limit | 100 concurrent | Retry 24h on failure | Messages queue if endpoint down [src1] |
| Salesforce (Platform Events) | 100K std; 10M+ add-on | 250K/hour (high-vol) | At-least-once, 24h replay | Replay by replayId [src1] |
| SAP Event Mesh | BTP plan dependent | Throttled per queue | Configurable retry + DLQ | [src2] |
| Oracle ERP Cloud | No published limit | OIC throttled | Best-effort, manual replay | Requires OIC |
| NetSuite (SuiteScript) | Governance-limited | 10 concurrent HTTP calls | No built-in retry | [src3] |
| D365 F&O | No hard limit | Azure throttled | Azure Service Bus DLQ | Failed events go to DLQ |
| D365 Business Central | No hard limit | 1,000 changes/30s batch | 36h retry (408,429,5xx) | Auto-deletes on non-retryable error [src4] |
| Workday | N/A (polling) | API rate limits apply | N/A | [src5] |
| IFS Cloud | No published limit | Server resources | Configurable | [src7] |
| Epicor Kinetic | No published limit | Server resources | No built-in retry | Custom BPM calls |
| Zoho | 500 (free) / 5,000 (paid) | Plan burst limits | 2x retry on failure | [src8] |
| Acumatica | No hard limit | GI refresh cycle | Configurable retries | [src6] |
| ERP System | Auth for Outbound Delivery | Callback Verification | Notes |
|---|---|---|---|
| Salesforce | N/A (SF initiates) | Session ID in SOAP header | Verify SF IP ranges [src1] |
| SAP Event Mesh | OAuth 2.0 (BTP) | HMAC signature (optional) | [src2] |
| Oracle ERP Cloud | OIC-managed | OIC connection auth | External endpoint auth in OIC |
| NetSuite | TBA or OAuth 2.0 | Custom header injection | [src3] |
| D365 F&O | Azure AD / SAS token | Azure Service Bus verify | Azure endpoint auth |
| D365 Business Central | N/A (BC initiates) | clientState secret in payload | Handshake required [src4] |
| Workday | N/A (polling) | N/A | [src5] |
| IFS Cloud | Custom header | No built-in verification | [src7] |
| Zoho | Custom header | No built-in HMAC | [src8] |
| Acumatica | Custom header/URL token | No built-in HMAC | [src6] |
START -- Need real-time outbound notifications from ERP
|-- Which ERP?
| |-- Salesforce
| | |-- Simple field-change notifications -> Outbound Messages (SOAP callback)
| | |-- Structured events with replay -> Platform Events + Pub/Sub API (gRPC)
| | +-- Full change tracking -> Change Data Capture + Pub/Sub API
| |-- SAP S/4HANA Cloud
| | |-- Have BTP subscription? -> SAP Event Mesh (AMQP or Webhook)
| | +-- No BTP? -> Poll OData API or use IDocs (legacy)
| |-- Oracle ERP Cloud
| | |-- Have OIC? -> Business Events -> OIC -> external webhook
| | +-- No OIC? -> Poll REST API (no native push available)
| |-- NetSuite
| | |-- Simple triggers -> User Event Script afterSubmit -> https.post()
| | |-- Workflow-driven -> Workflow Action Script -> https.post()
| | +-- High volume -> Scheduled Script with SuiteQL batch + push
| |-- D365 Finance & Operations
| | |-- Standard processes -> Business Events -> Azure endpoint
| | |-- Data-level changes -> Data Events -> Azure Service Bus
| | +-- Simple CRUD -> Dataverse webhooks (if using Dataverse)
| |-- D365 Business Central
| | +-- Any entity change -> Webhook Subscription API (REST, 30s batched)
| |-- Workday
| | |-- Payroll changes -> PECI (scheduled, not real-time)
| | +-- All other data -> Poll REST/SOAP API from external system
| |-- IFS Cloud
| | +-- Record changes -> Event Action -> REST POST to external URL
| |-- Epicor Kinetic
| | +-- Transaction events -> Custom BPM/Function -> REST POST
| |-- Sage Intacct
| | +-- Limited events -> Custom Sender or poll Web Services API
| |-- Zoho
| | +-- Workflow triggers -> Webhook action (HTTP POST, JSON)
| +-- Acumatica
| +-- Data changes -> Generic Inquiry + Push Notification (HTTP POST)
|-- Need guaranteed delivery?
| |-- YES -> Salesforce Platform Events (replay) or SAP Event Mesh (DLQ)
| +-- NO -> Any native push mechanism + custom retry
+-- Latency requirement?
|-- <5 seconds -> Salesforce, Zoho, D365 F&O, IFS Cloud, Acumatica
|-- <30 seconds -> D365 BC, SAP Event Mesh, Oracle (via OIC)
+-- Minutes acceptable -> Polling (any ERP), Workday PECI
| ERP System | Native HTTP Webhook? | Event Bus? | Polling Required? | Best Real-Time Option | Delivery Guarantee |
|---|---|---|---|---|---|
| Salesforce | Yes (Outbound Messages, SOAP) | Yes (Platform Events, CDC) | No | Platform Events + Pub/Sub API | At-least-once (replay) |
| SAP S/4HANA | Via Event Mesh (add-on) | Yes (Event Mesh, AEM) | Fallback | Event Mesh webhook subscription | Configurable (DLQ) |
| Oracle ERP Cloud | No (requires OIC) | Yes (Business Events) | Fallback | Business Events via OIC | Best-effort |
| NetSuite | Via SuiteScript (custom) | No | Fallback | User Event Script | No (custom retry) |
| D365 F&O | Via Azure endpoints | Yes (Business/Data Events) | No | Business Events -> Azure | At-least-once (Azure DLQ) |
| D365 BC | Yes (Subscription API) | No | No | Webhook Subscriptions (30s batch) | At-most-once (36h retry) |
| Workday | No | No | Yes (required) | Poll REST API | N/A |
| IFS Cloud | Via Event Actions | No | Fallback | Event Action -> REST POST | Best-effort |
| Epicor Kinetic | No (BPM workaround) | No | Fallback | BPM -> REST POST (custom) | No (custom retry) |
| Sage Intacct | No (Custom Sender) | No | Recommended | Poll Web Services API | N/A |
| Zoho | Yes (Workflow Webhooks) | No | No | Workflow Webhook POST | Best-effort (2 retries) |
| Acumatica | Yes (Push Notifications) | No | No | GI Push Notification | Configurable retries |
| Capability | Salesforce | SAP S/4HANA | Oracle ERP Cloud | NetSuite | D365 F&O | D365 BC | Workday | IFS Cloud | Epicor | Sage Intacct | Zoho | Acumatica |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Native outbound webhook | OM (SOAP) | No (add-on) | No (OIC) | No (custom) | No (Azure) | Yes | No | Event Actions | No (BPM) | No | Yes | Yes |
| Event bus | PE, CDC | Event Mesh | Biz Events | No | Biz/Data Events | No | No | No | No | No | No | No |
| Protocol | SOAP, gRPC | AMQP, HTTP | OIC | HTTPS | Azure SB | REST/JSON | N/A | REST | REST | REST | REST/JSON | REST/JSON |
| Setup complexity | Low-Med | High | High | Medium | Medium | Low | N/A | Medium | High | High | Low | Low-Med |
| Latency | 1–5s | 1–30s | 5–30s | 1–10s | 2–15s | ~30s | 1–15m | 2–10s | 2–15s | 5–30s | <5s | 1–10s |
| Delivery guarantee | At-least-once | Configurable | Best-effort | None | At-least-once | At-most-once | N/A | Best-effort | None | None | Best-effort | Configurable |
| Replay/recovery | Yes (24h) | Yes (DLQ) | Manual | No | Yes (Azure DLQ) | No | N/A | No | No | No | No | No |
| Event filtering | Object/field | Event type | Event ID | Script logic | Category | Resource | N/A | Event type | BPM logic | Trigger | Workflow | GI criteria |
| Max payload | 1 MB (PE) | 1 MB | Varies | Custom | 1 MB | Collection | N/A | Custom | Custom | Custom | Custom | Custom |
| Additional cost | High-vol PE lic. | BTP required | OIC required | Included | Azure costs | Included | N/A | Included | Included | Included | Plan-dep. | Included |
Classify your ERP into one of three tiers. [src1, src4, src6, src8]
Tier 1 — Native HTTP Webhooks (D365 BC, Zoho, Acumatica, Salesforce Outbound Messages): ERP pushes HTTP POST natively.
Tier 2 — Event Bus or Custom Script (Salesforce PE, SAP Event Mesh, Oracle via OIC, NetSuite SuiteScript, D365 F&O, IFS Cloud): Events emitted to bus or custom script pushes.
Tier 3 — Polling Required (Workday, Sage Intacct, Epicor without customization): No push mechanism.
Verify: Check the Quick Reference table to confirm your ERP's tier.
Example: D365 Business Central. [src4]
# Register a webhook subscription for Sales Orders in D365 Business Central
curl -X POST "https://{tenant}.api.businesscentral.dynamics.com/v2.0/{env}/api/v2.0/subscriptions" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"notificationUrl": "https://your-endpoint.example.com/webhook/d365bc",
"resource": "salesOrders",
"clientState": "your-secret-state-value"
}'
Verify: BC sends validation request → endpoint echoes validationToken → HTTP 200.
Example: Salesforce Platform Events via Pub/Sub API. [src1]
# Subscribe to Salesforce Platform Events via gRPC Pub/Sub API
import grpc
from salesforce_pubsub import PubSubClient
client = PubSubClient(
instance_url="https://yourorg.my.salesforce.com",
client_id="your_connected_app_id",
client_secret="your_connected_app_secret",
username="[email protected]"
)
for event in client.subscribe("/event/Order_Created__e", replay_preset="LATEST"):
print(f"Order created: {event.payload}")
Verify: Publish test Platform Event → subscriber receives within 1–5s.
Example: Workday. [src5]
# Poll Workday REST API for worker changes
import requests
def poll_workday_changes(tenant, token, last_poll_time):
url = f"https://wd5-impl-services1.workday.com/ccx/api/v1/{tenant}/workers"
headers = {"Authorization": f"Bearer {token}"}
params = {"limit": 100, "offset": 0}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json().get("data", [])
# Poll every 5 minutes -- store last_poll_time for recovery
Verify: Run poll → compare record counts with Workday UI.
| ERP | Error | Meaning | Resolution |
|---|---|---|---|
| Salesforce | OUTBOUND_MSG_FAILED | Endpoint unreachable/timeout | Check endpoint URL; SF retries 24h [src1] |
| D365 BC | Subscription deleted | Non-retryable error code | Fix endpoint to return 200 or 5xx only [src4] |
| NetSuite | SSS_REQUEST_LIMIT_EXCEEDED | Governance limit reached | Batch calls or use scheduled script [src3] |
| SAP | 429 | Rate limit exceeded | Increase Event Mesh plan [src2] |
| Acumatica | Push notification failed | Target URL unreachable | Verify URL in SM302000 [src6] |
| Zoho | WEBHOOK_LIMIT_REACHED | Daily quota exceeded | Upgrade plan or batch [src8] |
Always return 200 on success, 500 for temporary failures. Monitor subscription existence with periodic GET /subscriptions. [src4]Acknowledge immediately, process asynchronously in background. [src1]Queue to custom record, process in Scheduled Script (5,000 units). [src3]Switch to AMQP protocol for lower latency. [src2]Use Business Events with custom actions for true real-time needs. [src6]// BAD -- designing integration expecting simple HTTP POST from any ERP
app.post('/webhook/erp', (req, res) => {
const event = req.body; // Salesforce sends SOAP XML, not JSON
processERPEvent(event); // SAP sends via AMQP, not HTTP
res.status(200).send('OK'); // Workday doesn't send anything at all
});
// GOOD -- ERP-specific adapter pattern
const adapters = {
'd365bc': new WebhookAdapter({ format: 'json', verify: 'clientState' }),
'salesforce': new SOAPAdapter({ verify: 'sessionId', ipWhitelist: SF_RANGES }),
'zoho': new WebhookAdapter({ format: 'json', verify: 'header-token' }),
'acumatica': new WebhookAdapter({ format: 'json', verify: 'url-token' }),
'workday': new PollingAdapter({ interval: '5m', changeDetection: 'timestamp' }),
'sap': new AMQPAdapter({ queue: 'event-mesh', protocol: 'amqp10' }),
};
// BAD -- calling external API in afterSubmit for every record
function afterSubmit(context) {
var record = context.newRecord;
https.post({ url: WEBHOOK_URL, body: JSON.stringify(record) });
// Bulk import of 1,000 records = governance exhaustion
}
// GOOD -- queue records, send in batch via Scheduled Script
function afterSubmit(context) {
var queue = record.create({ type: 'customrecord_outbound_queue' });
queue.setValue({ fieldId: 'custrecord_record_id', value: context.newRecord.id });
queue.save(); // Cheap: ~10 governance units
}
// Scheduled Script processes queue with 5,000 governance units
// BAD -- register webhook once and forget
await registerWebhook(resource, notificationUrl);
// 3 days later: subscription silently expires
// GOOD -- renew D365 BC subscription before 3-day expiry
async function maintainSubscription(subscriptionId) {
const renewed = await fetch(`/subscriptions(${subscriptionId})`, {
method: 'PATCH',
body: JSON.stringify({
expirationDateTime: new Date(Date.now() + 3 * 86400000).toISOString()
})
});
if (renewed.status === 404) await registerWebhook(resource, notificationUrl);
}
// Run on a 2-day interval
Read the Cross-System Comparison table and implement per-ERP adapters. [src1, src2]For zero-loss, use systems with replay capability and implement idempotent processing. [src1, src4]Handle both single and collection notifications. [src4]Use Map/Reduce scripts (10,000 units) for bulk outbound. [src3]Design polling from the start. Use RaaS with timestamp filtering, poll every 5-15 min. [src5]Confirm BTP licensing before committing to event-driven SAP integration. [src2]# Salesforce: Check Outbound Message delivery status
# Setup -> Outbound Messages -> View Message Delivery Status
# D365 Business Central: List active webhook subscriptions
curl -H "Authorization: Bearer $BC_TOKEN" \
"https://{tenant}.api.businesscentral.dynamics.com/v2.0/{env}/api/v2.0/subscriptions"
# NetSuite: Check governance usage
# Customization -> Scripting -> Script Execution Log (filter by script type)
# SAP Event Mesh: Check webhook subscription status
curl -H "Authorization: Bearer $BTP_TOKEN" \
"https://enterprise-messaging-pubsub.cfapps.{region}.hana.ondemand.com/messagingrest/v1/subscriptions"
# Acumatica: Verify Push Notification config
# System -> Integration -> Push Notifications (SM302000) -- check Status column
# Zoho: Check webhook execution log
# Setup -> Automation -> Workflow Rules -> select rule -> View Webhook Log
| ERP | Feature | Introduced | Current Status | Notes |
|---|---|---|---|---|
| Salesforce | Outbound Messages | ~2006 | GA (legacy) | SOAP-only; Platform Events preferred |
| Salesforce | Platform Events | Spring '17 | GA | Replaced Streaming API |
| Salesforce | Pub/Sub API (gRPC) | Spring '22 | GA | Replaced CometD Streaming API |
| SAP | Event Mesh | 2020 | GA | Requires BTP; rebranded from Enterprise Messaging |
| SAP | Advanced Event Mesh | 2023 | GA | Enterprise tier (Solace-based) |
| Oracle ERP | Business Events | 2019 | GA | Requires OIC for external delivery |
| NetSuite | SuiteScript 2.0 | 2016 | GA | https module for outbound |
| D365 F&O | Business Events | 10.0.22 (2022) | GA | Azure endpoints |
| D365 F&O | Data Events | 10.0.39 (2024) | GA | More granular CRUD-level events |
| D365 BC | Webhook Subscriptions | v1.0 API (2018) | GA | 30s batching, 3-day expiry |
| IFS Cloud | Event Actions | 21R2 | GA | REST POST via workflow |
| Acumatica | Push Notifications | 2019 R1 | GA | Based on Generic Inquiries |
| Zoho | Workflow Webhooks | ~2018 | GA | Plan-based daily limits |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Comparing webhook support across multiple ERPs for architecture decisions | Deep-dive on single ERP's event system | ERP Event-Driven Comparison |
| Evaluating ERPs where real-time integration is a key requirement | Already chosen an ERP and need implementation details | System-specific API capability card |
| Determining if an ERP requires middleware for push notifications | Need webhook payload schemas and field mappings | System-specific integration playbook |
| Building iPaaS adapter layer supporting multiple ERPs | Only integrating with one ERP | Single-system event configuration guide |