ERP Error Handling & Retry Pattern Comparison: Salesforce vs SAP vs Oracle vs NetSuite vs Dynamics 365 vs Workday vs IFS vs Acumatica vs Business Central (2026)

Type: ERP Integration Systems: Salesforce, SAP S/4HANA, Oracle Fusion, NetSuite, Dynamics 365 F&O, Business Central, Workday, IFS Cloud, Acumatica Confidence: 0.85 Sources: 8 Verified: 2026-03-03 Freshness: evolving

TL;DR

System Profile

This card compares error handling, rate limit headers, retry-after patterns, partial success behavior, and error response formats across nine major ERP platforms as of early 2026. It focuses on cloud deployments and standard API surfaces (REST, OData, SOAP).

SystemRoleAPI SurfaceError Response Format
SalesforceCRM + PlatformREST, SOAP, Bulk API 2.0, CompositeJSON array of error objects
SAP S/4HANA CloudERPOData v2/v4, SOAPOData error JSON / XML
Oracle Fusion Cloud ERPERPREST, SOAP, FBDIJSON with code + message
Oracle NetSuiteERPREST, SuiteTalk SOAP, RESTlets, SuiteQLJSON error object (REST) / SOAP Fault (SOAP)
Dynamics 365 F&OERPOData v4, Custom ServicesOData error JSON with innererror
Dynamics 365 Business CentralERPREST (OData v4)OData error JSON
WorkdayHCM + FinancialsREST, SOAP, RaaSJSON errors array / SOAP Fault
IFS CloudERPOData, RESTOData-style JSON with code + message + details
AcumaticaERPREST, OData, Screen-BasedJSON with ExceptionMessage / custom error fields

API Surfaces & Capabilities

API SurfaceProtocolError Format429 SupportRetry-After HeaderPartial Success
Salesforce RESTHTTPS/JSON[{"errorCode":"...","message":"..."}]YesYes (seconds)No (all-or-nothing)
Salesforce CompositeHTTPS/JSONPer-subrequest status codesYesYesYes (per subrequest)
Salesforce Bulk API 2.0HTTPS/CSVPer-record success/failure filesYesYesYes (per record)
SAP OData v4HTTPS/JSON{"error":{"code":"...","message":"..."}}Infrastructure-dependentInfrastructure-dependentYes (via $batch)
Oracle Fusion RESTHTTPS/JSONRFC 7807 problem detailsYes (429)Not guaranteedNo
NetSuite RESTHTTPS/JSON{"type":"...","title":"...","status":429}Yes (429)Not guaranteedNo
NetSuite SuiteTalk SOAPHTTPS/XMLSOAP Fault with statusCodeNo (uses 403)NoYes (per-record in lists)
Dynamics 365 ODataHTTPS/JSONOData error + innererror chainYesYes (seconds)Yes (via $batch)
Business Central RESTHTTPS/JSON{"error":{"code":"...","message":"..."}}YesYes (seconds)No
Workday RESTHTTPS/JSON{"errors":[{"error":"..."}]}Yes (429)Not documentedNo
IFS ODataHTTPS/JSONOData-style JSON with details[]Not standardNoNo
Acumatica RESTHTTPS/JSON{"ExceptionMessage":"...","ExceptionType":"..."}No (500 on queue full)NoVia separate status check

Rate Limits & Quotas

Rate Limit Header Comparison

SystemRate Limit Status CodeRetry-After HeaderRate Limit HeadersError Body Format
Salesforce429 Too Many RequestsYes -- value in secondsNone standard; use /limits endpoint[{"errorCode":"REQUEST_LIMIT_EXCEEDED","message":"..."}]
SAP S/4HANADepends on API MgmtDepends on API Mgmt configOnly if SAP API Management deployedOData error JSON
Oracle Fusion429 Too Many RequestsNot reliably returnedNone standard{"code":"TooManyRequests","message":"User-rate limit exceeded."}
NetSuite REST429 Too Many RequestsNot reliably returnedNone standard{"status":429,"o:errorDetails":[{"o:errorCode":"CONCUR_LIMIT_EXCEEDED"}]}
NetSuite SOAP403 ForbiddenNoNoSOAP Fault: SSS_REQUEST_LIMIT_EXCEEDED
Dynamics 365 F&O429 Too Many RequestsYes -- value in secondsNone standard{"error":{"code":"429","message":"Number of requests exceeded the limit..."}}
Business Central429 Too Many RequestsYes -- value in secondsNone standard{"error":{"code":"too_many_requests","message":"..."}}
Workday429 Too Many RequestsNot publicly documentedNot documentedVaries by API surface
IFS Cloud429 (not standard)NoNoOData-style {"error":{"code":"...","message":"..."}}
Acumatica500 (queue full)NoNo{"ExceptionMessage":"The request queue is full..."}

Error Response Format Deep Dive

Salesforce REST API

[
  {
    "errorCode": "REQUEST_LIMIT_EXCEEDED",
    "message": "TotalRequests Limit exceeded.",
    "fields": []
  }
]

SAP S/4HANA OData v4

{
  "error": {
    "code": "/IWBEP/CM_V4_RUNTIME/021",
    "message": "Resource not found for segment 'InvalidEntity'",
    "@SAP.severity": "error",
    "target": "InvalidEntity",
    "details": []
  }
}

Oracle Fusion REST

{
  "type": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "User-rate limit exceeded.",
  "o:errorCode": "TooManyRequests"
}

NetSuite REST

{
  "type": "https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.30",
  "title": "Too Many Requests",
  "status": 429,
  "o:errorDetails": [
    {
      "detail": "Concurrency limit for this account has been exceeded.",
      "o:errorCode": "CONCUR_LIMIT_EXCEEDED"
    }
  ]
}

Dynamics 365 F&O

{
  "error": {
    "code": "429",
    "message": "Number of requests exceeded the limit of 6000 over the time window of 300 seconds.",
    "innererror": {
      "message": "Rate limit exceeded",
      "type": "Microsoft.Dynamics.Platform.Integration.Services.OData.AxRateLimitException"
    }
  }
}

IFS Cloud

{
  "error": {
    "code": "DATABASE_ERROR",
    "message": "Operation failed due to a database constraint violation.",
    "details": [
      {
        "code": "ORA-20124",
        "message": "Error.NULLVALUE: Field [NAME] is mandatory and requires a value."
      }
    ]
  }
}

Acumatica

{
  "ExceptionMessage": "Error: The following validation errors occurred: 'Customer ID' cannot be empty.",
  "ExceptionType": "PX.Api.ContractBased.OutcomeEntityHasErrorsException",
  "StackTrace": "..."
}

Authentication

SystemAuth Error CodeError FormatCommon Auth Errors
Salesforce401 Unauthorized[{"errorCode":"INVALID_SESSION_ID","message":"..."}]Token expired, invalid connected app, IP restrictions
SAP S/4HANA401 / 403OData error JSONCertificate mismatch, scope insufficient, XSRF token missing
Oracle Fusion401 UnauthorizedJSON problem detailsBasic auth credentials wrong, OAuth token expired
NetSuite401 UnauthorizedJSON error objectTBA token revoked, OAuth token expired, invalid consumer key
Dynamics 365401 / 403OData error JSONEntra ID token expired, insufficient app permissions
Business Central401 UnauthorizedOData error JSONOAuth token expired, wrong tenant
Workday401 UnauthorizedJSON or SOAP FaultISU credentials wrong, OAuth refresh token expired
IFS Cloud401 / 403JSON errorInvalid API key, insufficient permissions
Acumatica401 UnauthorizedJSONLogin limit exceeded, session expired

Authentication Gotchas

Constraints

Integration Pattern Decision Tree

START -- How should I handle errors across ERP integrations?
|
+-- What type of error are you handling?
|   |
|   +-- Rate limiting (HTTP 429)
|   |   +-- Does the ERP return Retry-After header?
|   |   |   +-- YES (Salesforce, D365 F&O, Business Central)
|   |   |   |   --> Use Retry-After value directly
|   |   |   +-- NO (Oracle, NetSuite, Workday, IFS)
|   |   |       --> Exponential backoff: 2^n seconds, max 60s, with jitter
|   |   +-- Does the ERP use 429?
|   |       +-- YES (all except SAP, Acumatica, NetSuite SOAP) --> Standard handler
|   |       +-- NO -- SAP: gateway-dependent
|   |       +-- NO -- Acumatica: returns 500 with queue-full message
|   |       +-- NO -- NetSuite SOAP: returns 403
|   |
|   +-- Business logic errors (400/422)
|   |   +-- Retryable? --> Retry with backoff (3 attempts max)
|   |   +-- Non-retryable --> Log, skip, dead letter queue
|   |
|   +-- Partial success (bulk/batch operations)
|   |   +-- Salesforce Bulk API 2.0 --> Download successfulResults + failedResults
|   |   +-- Salesforce Composite --> Check each subrequest httpStatusCode
|   |   +-- SAP $batch --> Check each changeset status
|   |   +-- NetSuite SOAP --> Check writeResponseList per record
|   |   +-- Dynamics 365 $batch --> Check each response in multipart body
|   |   +-- Acumatica --> Call process status endpoint separately
|   |
|   +-- Server errors (500/503) --> Always retryable with exponential backoff
|
+-- Error tolerance?
    +-- Zero-loss --> Idempotency keys + dead letter queue + reconciliation
    +-- Best-effort --> Exponential backoff with max retries, then log and alert

Quick Reference

CapabilitySalesforceSAP S/4HANAOracle FusionNetSuiteD365 F&OD365 BCWorkdayIFS CloudAcumatica
Rate limit HTTP code429Gateway-dependent429429 (REST) / 403 (SOAP)429429429Non-standard500 (queue full)
Retry-After headerYesGateway-dependentNot guaranteedNot guaranteedYesYesNot documentedNoNo
Error formatJSON arrayOData JSONRFC 7807 JSONJSON + o:errorDetailsOData + innererrorOData JSONJSON errors arrayOData + detailsException JSON
Partial successYes (Bulk, Composite)Yes ($batch)NoYes (SOAP lists)Yes ($batch)NoNoNoSeparate status call
Error code fielderrorCodeerror.codeo:errorCodeo:errorCodeerror.codeerror.codeerrorerror.codeExceptionType
Field-level errorsYes (fields[])Partial (target)NoPartialYes (innererror)NoYes (field)Yes (details[])No
Governor/transaction errorsYes (extensive)NoNoYes (governance units)Execution time poolNoNoNoNo
Idempotency supportSforce-Call-OptionsIf-Match ETagNone standardNone standardIf-Match ETagIf-Match ETagNone standardIf-Match ETagNone standard
Recommended backoffRetry-After value2^n seconds2^n, max 60s2^n + jitterRetry-After valueRetry-After value2^n + jitter2^n, max 60s60s fixed then 2^n

Step-by-Step Integration Guide

1. Detect the error type from HTTP status code

Every ERP returns standard HTTP status codes, but the meaning and recommended handling differs. [src1, src2]

function classifyError(httpStatus, body, erpSystem) {
  if (httpStatus === 429) return 'RATE_LIMITED';
  if (httpStatus === 403 && erpSystem === 'netsuite_soap') return 'RATE_LIMITED';
  if (httpStatus === 500 && erpSystem === 'acumatica'
      && body?.ExceptionMessage?.includes('queue is full')) return 'RATE_LIMITED';
  if (httpStatus === 401) return 'AUTH_EXPIRED';
  if (httpStatus === 403) return 'FORBIDDEN';
  if (httpStatus >= 400 && httpStatus < 500) return 'CLIENT_ERROR';
  if (httpStatus >= 500) return 'SERVER_ERROR';
  return 'SUCCESS';
}

Verify: Send a request with an expired token to each ERP -> all should return AUTH_EXPIRED.

2. Extract retry timing from response headers

Parse the Retry-After header when available; fall back to exponential backoff. [src1, src2, src5]

function getRetryDelay(response, attempt, erpSystem) {
  const retryAfter = response.headers.get('Retry-After');
  if (retryAfter) {
    const seconds = parseInt(retryAfter, 10);
    if (!isNaN(seconds)) return seconds * 1000;
    const date = new Date(retryAfter);
    if (!isNaN(date)) return Math.max(0, date - Date.now());
  }
  const baseDelays = {
    salesforce: 2000, sap: 3000, oracle_fusion: 5000,
    netsuite: 2000, dynamics365: 2000, business_central: 2000,
    workday: 5000, ifs: 3000, acumatica: 60000,
  };
  const base = baseDelays[erpSystem] || 2000;
  const delay = base * Math.pow(2, attempt);
  const jitter = delay * 0.2 * Math.random();
  return Math.min(delay + jitter, 120000);
}

Verify: Simulate a 429 with Retry-After: 30 header -> function should return 30000.

3. Implement the retry loop with ERP-aware logic

Combine classification, delay calculation, and max-retry policies. [src1, src2, src3]

async function fetchWithErpRetry(url, options, erpSystem, maxRetries = 5) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);
    const body = await response.json().catch(() => null);
    const errorType = classifyError(response.status, body, erpSystem);

    if (errorType === 'SUCCESS') return { response, body };
    if (errorType === 'AUTH_EXPIRED') {
      options.headers.Authorization = await refreshToken(erpSystem);
      continue;
    }
    if (errorType === 'RATE_LIMITED' || errorType === 'SERVER_ERROR') {
      if (attempt === maxRetries) throw new Error(`${errorType} after ${maxRetries} retries`);
      const delay = getRetryDelay(response, attempt, erpSystem);
      await new Promise(r => setTimeout(r, delay));
      continue;
    }
    throw new Error(`Non-retryable error from ${erpSystem}: ${JSON.stringify(body)}`);
  }
}

Verify: Mock a 429 for first 2 calls, 200 on third -> function should succeed after 2 retries.

4. Handle partial success in bulk operations

Each ERP has a different model for reporting per-record outcomes in batch operations. [src7, src4]

async function handleBulkResult(erpSystem, jobId, response) {
  switch (erpSystem) {
    case 'salesforce_bulk':
      // Separate endpoints for success/failure
      const failed = await fetch(`/services/data/v62.0/jobs/ingest/${jobId}/failedResults`);
      const succeeded = await fetch(`/services/data/v62.0/jobs/ingest/${jobId}/successfulResults`);
      return { failed: await failed.text(), succeeded: await succeeded.text() };
    case 'salesforce_composite':
      return {
        failed: response.compositeResponse.filter(r => r.httpStatusCode >= 400),
        succeeded: response.compositeResponse.filter(r => r.httpStatusCode < 400)
      };
    case 'netsuite_soap':
      return {
        failed: response.writeResponseList.writeResponse.filter(r => !r.status.isSuccess),
        succeeded: response.writeResponseList.writeResponse.filter(r => r.status.isSuccess)
      };
    case 'acumatica':
      return await fetch(`/entity/Default/24.200.001/${jobId}/status`).then(r => r.json());
    default:
      return { allOrNothing: true, response };
  }
}

Verify: Submit a Salesforce Bulk API job with intentional invalid records -> failedResults endpoint should return specific records with error details.

Code Examples

Python: Universal ERP retry handler

# Input:  ERP system name, request URL, headers, optional body
# Output: Successful response or raises after max retries

import time, random, requests  # requests==2.31.0

ERP_CONFIGS = {
    "salesforce": {"retry_codes": [429, 503], "max_retries": 5, "base_delay": 2},
    "sap": {"retry_codes": [429, 503], "max_retries": 5, "base_delay": 3},
    "oracle_fusion": {"retry_codes": [429, 503], "max_retries": 5, "base_delay": 5},
    "netsuite_rest": {"retry_codes": [429, 503], "max_retries": 5, "base_delay": 2},
    "netsuite_soap": {"retry_codes": [403, 503], "max_retries": 5, "base_delay": 2},
    "dynamics365": {"retry_codes": [429, 503], "max_retries": 5, "base_delay": 2},
    "business_central": {"retry_codes": [429, 503], "max_retries": 5, "base_delay": 2},
    "workday": {"retry_codes": [429, 503], "max_retries": 5, "base_delay": 5},
    "ifs": {"retry_codes": [429, 503], "max_retries": 5, "base_delay": 3},
    "acumatica": {"retry_codes": [500, 503], "max_retries": 3, "base_delay": 60},
}

def erp_request(erp_system, method, url, headers=None, json=None, **kwargs):
    config = ERP_CONFIGS.get(erp_system, {"retry_codes": [429, 503], "max_retries": 5, "base_delay": 2})
    for attempt in range(config["max_retries"] + 1):
        resp = requests.request(method, url, headers=headers, json=json, **kwargs)
        if resp.status_code < 400:
            return resp
        is_retryable = resp.status_code in config["retry_codes"]
        if erp_system == "acumatica" and resp.status_code == 500:
            body = resp.json() if resp.text else {}
            if "queue is full" not in str(body.get("ExceptionMessage", "")):
                is_retryable = False
        if not is_retryable or attempt == config["max_retries"]:
            resp.raise_for_status()
        retry_after = resp.headers.get("Retry-After")
        if retry_after and retry_after.isdigit():
            delay = int(retry_after)
        else:
            delay = config["base_delay"] * (2 ** attempt)
            delay += random.uniform(0, delay * 0.2)
            delay = min(delay, 120)
        time.sleep(delay)
    raise requests.exceptions.RetryError(f"Max retries exceeded for {erp_system}")

JavaScript/Node.js: Parse ERP-specific error responses

// Input:  HTTP response object, ERP system identifier
// Output: Normalized error object { code, message, retryable, fields, raw }

function parseErpError(response, body, erpSystem) {
  const normalized = { httpStatus: response.status, code: 'UNKNOWN_ERROR',
    message: 'An unknown error occurred', retryable: false, fields: [], raw: body };
  switch (erpSystem) {
    case 'salesforce':
      if (Array.isArray(body)) {
        normalized.code = body[0]?.errorCode || 'UNKNOWN';
        normalized.message = body[0]?.message || '';
        normalized.fields = body[0]?.fields || [];
        normalized.retryable = ['REQUEST_LIMIT_EXCEEDED','SERVER_UNAVAILABLE'].includes(normalized.code);
      }
      break;
    case 'netsuite':
      const details = body?.['o:errorDetails']?.[0];
      normalized.code = details?.['o:errorCode'] || 'UNKNOWN';
      normalized.message = details?.detail || body?.title || '';
      normalized.retryable = [429, 403].includes(response.status);
      break;
    case 'dynamics365':
    case 'business_central':
      normalized.code = body?.error?.code || 'UNKNOWN';
      normalized.message = body?.error?.message || '';
      normalized.retryable = response.status === 429 || response.status >= 500;
      break;
    case 'acumatica':
      normalized.code = body?.ExceptionType || 'UNKNOWN';
      normalized.message = body?.ExceptionMessage || '';
      normalized.retryable = normalized.message.includes('queue is full');
      break;
    // ... other ERPs follow similar patterns
  }
  return normalized;
}

cURL: Test error responses from each ERP

# === SALESFORCE: Check remaining API limits ===
curl -s -H "Authorization: Bearer $SF_TOKEN" \
  "https://yourInstance.salesforce.com/services/data/v62.0/limits" \
  | jq '{DailyApiRequests: {Max, Remaining}}'

# === DYNAMICS 365 F&O: Check throttle status ===
curl -s -D - -o /dev/null -H "Authorization: Bearer $D365_TOKEN" \
  "https://your-instance.operations.dynamics.com/data/Customers?\$top=1"
# Look for: Retry-After header in 429 responses

# === NETSUITE: Test concurrency limit response ===
curl -s -w "\nHTTP Status: %{http_code}\n" -H "Authorization: OAuth ..." \
  "https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/record/v1/customer/1"
# On 429: {"status":429,"o:errorDetails":[{"o:errorCode":"CONCUR_LIMIT_EXCEEDED"}]}

Error Handling & Failure Points

Common Error Codes

SystemCodeHTTP StatusMeaningRetryableResolution
SalesforceREQUEST_LIMIT_EXCEEDED429Daily API quota exceededYesWait for Retry-After; buy API call packs
SalesforceUNABLE_TO_LOCK_ROW400Record lock contentionYesRetry with jitter; avoid parallel updates to same record
SalesforceGOVERNOR_LIMIT500Transaction limit exceededNoBulkify code; reduce SOQL/DML per transaction
SAP/IWBEP/CM_V4_RUNTIME/021404Entity not foundNoVerify entity set name and API version
Oracle FusionTooManyRequests429Rate limit exceededYesExponential backoff; switch to FBDI for bulk
NetSuiteCONCUR_LIMIT_EXCEEDED429Concurrency slots exhaustedYesReduce parallel calls; reserve dedicated slots
NetSuiteSSS_REQUEST_LIMIT_EXCEEDED429/403Per-user rate limit exceededYesThrottle request rate; check governance dashboard
D365 F&O429429Service protection limitYesUse Retry-After header value
D365 BCtoo_many_requests429Per-user rate limitYesUse Retry-After header value
IFSDATABASE_ERROR500Database constraint violationNoFix data; check mandatory fields
AcumaticaPX.Data.PXException500Queue full or business logicMaybeIf queue-full: wait 60s and retry; otherwise: fix data

Failure Points in Production

Anti-Patterns

Wrong: Using a single 429 handler for all ERPs

// BAD -- assumes all ERPs use HTTP 429 for rate limiting
async function callErp(url, options) {
  const resp = await fetch(url, options);
  if (resp.status === 429) {
    await sleep(5000); // Fixed 5-second wait
    return callErp(url, options);
  }
  return resp;
}
// Misses: NetSuite SOAP (403), Acumatica (500), SAP (gateway-dependent)
// Also: no max retries = infinite loop risk

Correct: ERP-aware error classification with proper backoff

// GOOD -- handles each ERP's unique error signaling
async function callErp(url, options, erp, attempt = 0) {
  const MAX_RETRIES = 5;
  const resp = await fetch(url, options);
  const body = await resp.json().catch(() => null);
  const isRateLimited = resp.status === 429 ||
    (erp === 'netsuite_soap' && resp.status === 403) ||
    (erp === 'acumatica' && resp.status === 500
     && body?.ExceptionMessage?.includes('queue'));
  if (isRateLimited && attempt < MAX_RETRIES) {
    const delay = getRetryDelay(resp, attempt, erp);
    await sleep(delay);
    return callErp(url, options, erp, attempt + 1);
  }
  return { resp, body };
}

Wrong: Ignoring partial success in bulk operations

// BAD -- assumes bulk operation is all-or-nothing
const result = await sfBulkApi.createJob(records);
if (result.state === 'JobComplete') {
  console.log('All records imported successfully!');
  // WRONG: some records may have failed within a "completed" job
}

Correct: Always check per-record results in bulk operations

// GOOD -- check both success and failure results
const result = await sfBulkApi.createJob(records);
if (result.state === 'JobComplete') {
  const failures = await sfBulkApi.getFailedResults(result.id);
  const successes = await sfBulkApi.getSuccessfulResults(result.id);
  console.log(`Imported: ${successes.length}, Failed: ${failures.length}`);
  if (failures.length > 0) {
    await deadLetterQueue.push(failures.map(f => ({
      record: f, error: f.sf__Error, timestamp: new Date().toISOString()
    })));
  }
}

Wrong: Treating SAP 403 as authorization failure

// BAD -- SAP 403 often means missing XSRF token, not auth failure
if (resp.status === 403) {
  throw new Error('User not authorized');
  // Could be a missing X-CSRF-Token!
}

Correct: Check for XSRF token requirement on SAP 403

// GOOD -- differentiate between auth failure and missing XSRF token
if (resp.status === 403) {
  const body = await resp.json().catch(() => null);
  if (body?.error?.code?.includes('CSRF') || !options.headers['X-CSRF-Token']) {
    const tokenResp = await fetch(url, {
      method: 'HEAD',
      headers: { ...options.headers, 'X-CSRF-Token': 'Fetch' }
    });
    options.headers['X-CSRF-Token'] = tokenResp.headers.get('X-CSRF-Token');
    return callErp(url, options, 'sap'); // Retry with new token
  }
  throw new Error('Authorization failure: check user roles and permissions');
}

Common Pitfalls

Diagnostic Commands

# === SALESFORCE: Check remaining daily API quota ===
curl -s -H "Authorization: Bearer $SF_TOKEN" \
  "https://yourInstance.salesforce.com/services/data/v62.0/limits" \
  | jq '{DailyApiRequests: {Max, Remaining}}'

# === SALESFORCE: Check Bulk API job failures ===
curl -s -H "Authorization: Bearer $SF_TOKEN" \
  "https://yourInstance.salesforce.com/services/data/v62.0/jobs/ingest/$JOB_ID/failedResults"

# === NETSUITE: Check concurrency governance ===
# Navigate: Setup > Integration > Integration Management > Integration Governance

# === DYNAMICS 365 F&O: Monitor throttle events ===
# LCS > Environment Monitoring > Raw Logs > Filter: ThrottledRequests
curl -s -D - -o /dev/null -H "Authorization: Bearer $D365_TOKEN" \
  "https://your-instance.operations.dynamics.com/data/Customers?\$top=1"

# === BUSINESS CENTRAL: Test rate limit handling ===
curl -s -w "\nHTTP: %{http_code}\nRetry-After: %header{Retry-After}\n" \
  -H "Authorization: Bearer $BC_TOKEN" \
  "https://api.businesscentral.dynamics.com/v2.0/$TENANT_ID/$ENVIRONMENT/api/v2.0/companies"

# === IFS: Check error response format ===
curl -s -H "Authorization: Bearer $IFS_TOKEN" \
  "https://your-ifs-instance.ifs.cloud/main/ifsapplications/projection/v1/InvalidEndpoint" \
  | jq '{code: .error.code, message: .error.message, details: .error.details}'

Cross-System Comparison

CapabilitySalesforceSAP S/4HANAOracle FusionNetSuiteD365 F&OD365 BCWorkdayIFS CloudAcumatica
Rate Limit HTTP Code429Gateway-dependent429429 (REST) / 403 (SOAP)429429429Non-standard500 (queue full)
Retry-After HeaderYes (seconds)Gateway-dependentNot guaranteedNot guaranteedYes (seconds)Yes (seconds)Not documentedNoNo
Error JSON StructureArray of objectsOData errorRFC 7807o:errorDetailsOData + innererrorOData errorErrors arrayOData + detailsException object
Partial Success (Bulk)Yes (Bulk, Composite)Yes ($batch)NoYes (SOAP lists)Yes ($batch)NoNoNoSeparate status endpoint
Field-Level Error InfoYes (fields[])Partial (target)NoPartialYes (innererror)NoYes (field)Yes (details[])No
Governor/Transaction ErrorsYes (extensive)NoNoYes (governance units)Execution time poolNoNoNoNo
Idempotency MechanismSforce-Call-OptionsIf-Match ETagNone standardNone standardIf-Match ETagIf-Match ETagNone standardIf-Match ETagNone standard
Error Documentation QualityExcellentGood (OData)ModerateGoodGoodGoodPoorModerateModerate
Recommended BackoffRetry-After value2^n seconds2^n, max 60s2^n + jitterRetry-After valueRetry-After value2^n + jitter2^n, max 60s60s fixed then 2^n

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Designing retry/error handling for multi-ERP middlewareNeed rate limit quotas and thresholdsERP Rate Limits Comparison
Building a universal ERP integration adapterNeed dead letter queue architectureError Handling & Dead Letter Queues
Debugging why retry logic works on one ERP but not anotherNeed authentication flow detailsERP Authentication Comparison
Comparing ERPs for integration resilienceNeed single-system deep diveSystem-specific API capability card
Implementing observability across ERP integrationsNeed bulk import comparisonERP Bulk Import Comparison

Important Caveats

Related Units