Salesforce + D365 Finance & Operations Integration: Account/Order Sync, OData, Middleware

Type: ERP Integration Systems: Salesforce (API v62.0) + D365 F&O (10.0.40+) Confidence: 0.84 Sources: 8 Verified: 2026-03-07 Freshness: 2026-03-07

TL;DR

System Profile

This playbook covers the end-to-end integration between Salesforce (CRM, source of truth for leads, opportunities, accounts, and customer-facing data) and Microsoft Dynamics 365 Finance & Operations (ERP, source of truth for financial master data, pricing, inventory, sales orders, and invoices). It applies to D365 F&O version 10.0.38+ (cloud only). For organizations using D365 Business Central instead of F&O, the integration surface differs significantly.

The architecture requires a middleware layer. Unlike the Microsoft-to-Microsoft stack (where Dual Write provides native near-real-time sync between D365 CE and D365 F&O), Salesforce-to-D365-F&O has no native connector. You must build the bridge using Azure Integration Services, MuleSoft, Boomi, or custom OData clients.

SystemRoleAPI SurfaceDirection
Salesforce (API v62.0)CRM -- source of truth for accounts, contacts, opportunitiesREST API, Bulk API 2.0, Platform Events, CDCOutbound (account/order push) + Inbound (pricing/invoice pull)
D365 F&O (10.0.40+)ERP -- financial master, pricing, inventory, fulfillmentOData v4, Custom Services, Batch Data API (DMF), Business EventsInbound (customer/order creation) + Outbound (pricing/invoice/inventory)
Middleware (Azure / MuleSoft / Boomi)Integration orchestratorConnectors for both systemsBidirectional routing, transformation, error handling

API Surfaces & Capabilities

Salesforce Side

API SurfaceProtocolBest ForMax Records/RequestRate LimitReal-time?Bulk?
REST APIHTTPS/JSONIndividual record CRUD, <2K records2,000 per SOQL query100K+ calls/24hYesNo
Bulk API 2.0HTTPS/CSVETL, data migration, >2K records150M per file15,000 batches/24hNoYes
SOAP APIHTTPS/XMLMetadata operations, legacy2,000 recordsShared with REST limitYesNo
Composite APIHTTPS/JSONMulti-object ops in one call25 subrequestsCounts as 1 API callYesNo
Platform EventsCometD/gRPCReal-time event notificationsN/A200K-1M events/24hYesN/A
Change Data CaptureCometDTrack field-level changesN/A72h replay windowYesN/A
[src4]

D365 Finance & Operations Side

API SurfaceProtocolBest ForMax Records/RequestRate LimitReal-time?Bulk?
OData v4HTTPS/JSONStandard CRUD on data entitiesConfigurable via $top6,000 req/5min per userYesVia $batch
Custom ServicesHTTPS/JSON or SOAPCustom business logic endpointsVariesSame throttling as ODataYesNo
Batch Data API (DMF)HTTPS/JSONLarge-volume async import/exportMillions (package-based)Exempt from OData throttlingNo (async)Yes
Business EventsAzure Service Bus / Event GridReal-time event notificationsN/APer-event processingYesN/A
[src1, src2]

Rate Limits & Quotas

Salesforce Per-Request Limits

Limit TypeValueApplies ToNotes
Max records per SOQL query2,000REST/SOAP APIUse queryMore/nextRecordsUrl for pagination
Max request body size50 MBREST API
Max composite subrequests25Composite APIAll-or-nothing by default
Max batch file size150 MBBulk API 2.0Split larger files
Max records per batch10,000Bulk API 2.0

Salesforce Rolling / Daily Limits

Limit TypeValueWindowEdition Differences
Total API calls100,000 base24h rollingEnterprise: +1,000/license; Unlimited: +5,000/license; Developer: 15,000 total
Concurrent API requests25Per orgDeveloper/Trial: 5
Bulk API batches15,00024h rollingShared across editions
Streaming API events200,000-1,000,00024hEnterprise: 200K; Unlimited: 1M

Salesforce Governor Limits (Per Transaction)

Limit TypePer-Transaction ValueNotes
SOQL queries100Includes queries from triggers
DML statements150Each insert/update/delete counts as 1
Callouts (HTTP)100External HTTP requests within transaction
CPU time10,000 ms (sync) / 60,000 ms (async)Exceeded = transaction abort
Heap size6 MB (sync) / 12 MB (async)

D365 F&O Service Protection Limits

Limit TypeValueWindowNotes
Number of requests6,0005-minute sliding windowPer user, per application ID, per web server
Combined execution time1,200 seconds (20 min)5-minute sliding windowPer user, per web server
Concurrent requests52Per userPer web server instance
Resource-based throttlingDynamicCPU/memory-based429 response when server resources exceed threshold

Important: D365 F&O exempts DMF/DIXF, Recurring Integrations, Data Integrator, and Virtual Tables from OData throttling. For high-volume batch loads, use the Batch Data API instead of OData. [src2]

Authentication

Salesforce Authentication

FlowUse WhenToken LifetimeRefresh?Notes
OAuth 2.0 JWT BearerServer-to-server (recommended)Session timeout (2h default)New JWT per requestRequires connected app with digital certificate
OAuth 2.0 Client CredentialsServer-to-server (simpler)2hNo (new token per request)Spring '23+; no user context
OAuth 2.0 Web Server FlowUser-context operationsAccess: 2h; Refresh: until revokedYesRequires callback URL

D365 F&O Authentication

FlowUse WhenToken LifetimeRefresh?Notes
Azure AD/Entra ID Client CredentialsServer-to-server (recommended)Default 1h (configurable)YesRegister app in Azure portal; assign D365 F&O environment URL as audience
Azure AD Authorization CodeUser-context interactive flowsAccess: 1h; Refresh: 90 daysYesRequires redirect URI
S2S via Entra Managed IdentityAzure-hosted integrationsAuto-managedAutomaticOnly for Azure-hosted workloads

Authentication Gotchas

Constraints

Integration Pattern Decision Tree

START -- Salesforce <-> D365 F&O Integration
|
+-- What's the primary process?
|   +-- Account/Customer sync
|   |   +-- Volume < 1,000/day? --> OData real-time upserts
|   |   +-- Volume > 1,000/day? --> Batch Data API (DMF) on schedule
|   |   +-- ALWAYS assign External ID fields on both sides FIRST
|   +-- Opportunity-to-Order
|   |   +-- Event-driven: SF Platform Event on Close --> Middleware --> D365 OData
|   |   +-- Real-time (sub-minute): recommended for order accuracy
|   +-- Price List / Product sync (D365 --> Salesforce)
|   |   +-- Scheduled batch: D365 OData query --> SF Bulk API 2.0 upsert
|   +-- Invoice pushback (D365 --> Salesforce)
|       +-- D365 Business Event on invoice posting --> middleware --> SF REST upsert
|
+-- Which middleware?
|   +-- Azure-native --> Logic Apps + Service Bus + Event Grid
|   +-- Salesforce-native --> MuleSoft (pre-built D365 connector)
|   +-- Multi-cloud --> Boomi, Workato, Celigo
|
+-- Error tolerance?
    +-- Zero-loss (financial) --> Dead letter queue + idempotent retries
    +-- Best-effort (reporting) --> Retry 3x with backoff, log and skip

Quick Reference

StepSourceTriggerTargetData ObjectsFailure Handling
1. Account/Customer syncSalesforceAccount created/updatedD365 F&OCustCustomerV3Retry 3x, then DLQ
2. Contact syncSalesforceContact created/updatedD365 F&OsmmContactPersonV2Retry 3x, skip on dup
3. Opportunity-to-OrderSalesforceStage = Closed WonD365 F&OSalesOrderHeaderV2 + LinesRetry 3x, then manual
4. Product/Price syncD365 F&OScheduled / Business EventSalesforceProduct2 + PricebookEntryFull refresh upsert
5. Inventory checkD365 F&OOn-demand calloutSalesforceCustom objectReal-time OData read
6. Invoice pushbackD365 F&OInvoice postedSalesforceCustom Invoice__cRetry 3x, then recon batch
7. Payment status syncD365 F&OScheduled (hourly)SalesforceCustom field on AccountUpsert via External ID

Step-by-Step Integration Guide

1. Register applications and configure authentication

Set up OAuth credentials on both sides before building any data flows. [src1, src4]

# Test Salesforce JWT auth
curl -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  -d "assertion=${SALESFORCE_JWT_TOKEN}"

# Test D365 F&O auth
curl -X POST "https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" \
  -d "client_id=${CLIENT_ID}" \
  -d "client_secret=${CLIENT_SECRET}" \
  -d "scope=https://${D365_ENVIRONMENT}.operations.dynamics.com/.default" \
  -d "grant_type=client_credentials"

Verify: Both calls return {"access_token": "..."}.

2. Create External ID fields and data entity mappings

Create External ID fields on both systems to enable idempotent upserts. [src6]

# Salesforce: verify External ID field exists
curl -H "Authorization: Bearer ${SF_TOKEN}" \
  "${INSTANCE_URL}/services/data/v62.0/sobjects/Account/describe" \
  | jq '.fields[] | select(.name == "D365_Customer_Number__c")'

# D365 F&O: verify entity is OData-accessible
curl -H "Authorization: Bearer ${D365_TOKEN}" \
  "https://${D365_ENV}.operations.dynamics.com/data/CustomersV3?\$top=1"

Verify: Salesforce returns field metadata with "externalId": true. D365 returns a JSON array with at least one customer record.

3. Build Account/Customer bidirectional sync

Salesforce Account is the sales-facing master; D365 F&O Customer is the financial master. [src5, src6]

Verify: Create a test account in Salesforce sandbox and verify it appears in D365 F&O within expected latency.

4. Implement Opportunity-to-Order handoff

Salesforce opportunity closure triggers D365 F&O sales order creation. [src5, src7]

Verify: Close an Opportunity in Salesforce sandbox. Confirm the Sales Order appears in D365 F&O with matching line items.

5. Configure Price List and Product sync (D365 --> Salesforce)

Products and pricing are mastered in D365 F&O and pushed to Salesforce for sales team visibility. [src6, src7]

# D365: query released products
curl -H "Authorization: Bearer ${D365_TOKEN}" \
  "https://${D365_ENV}.operations.dynamics.com/data/ReleasedProductsV2?\$select=ItemNumber,ProductName&\$top=1000"

# Salesforce: upsert product via External ID
curl -X PATCH -H "Authorization: Bearer ${SF_TOKEN}" \
  -H "Content-Type: application/json" \
  "${INSTANCE_URL}/services/data/v62.0/sobjects/Product2/D365_Item_Number__c/ITEM001" \
  -d '{"Name": "Widget A", "ProductCode": "ITEM001", "IsActive": true}'

Verify: Compare product count in D365 F&O vs Salesforce after sync.

6. Build Invoice pushback (D365 --> Salesforce)

Push invoice data from D365 back to Salesforce so sales reps see payment status. [src5, src7]

Verify: Post a test invoice in D365 F&O. Confirm the Invoice__c record appears on the corresponding Account in Salesforce.

7. Implement error handling and retry logic

Production integrations must handle D365 F&O 429 throttling and Salesforce API limits gracefully. [src2]

// Node.js: D365 F&O OData call with retry on 429
const axios = require('axios'); // v1.6+

async function d365ODataRequest(method, url, data, token, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await axios({ method, url, data,
        headers: { 'Authorization': `Bearer ${token}`,
                   'Content-Type': 'application/json',
                   'OData-MaxVersion': '4.0', 'OData-Version': '4.0' },
        timeout: 30000 });
      return response.data;
    } catch (error) {
      if (error.response?.status === 429) {
        const retryAfter = parseInt(error.response.headers['retry-after'] || '30');
        const backoff = retryAfter * 1000 * Math.pow(1.5, attempt);
        await new Promise(resolve => setTimeout(resolve, backoff));
        continue;
      }
      if (error.response?.status >= 500 && attempt < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
        continue;
      }
      throw error;
    }
  }
  throw new Error(`Max retries exceeded for ${url}`);
}

Verify: Simulate rapid OData requests. Confirm retry logic pauses and resumes successfully.

Code Examples

Python: Bidirectional Account/Customer Sync

# Input:  Salesforce Account ID or D365 Customer Number
# Output: Synced record on the target system

import requests  # v2.31+

class SfD365Sync:
    def __init__(self, sf_token, sf_instance, d365_token, d365_env):
        self.sf_headers = {"Authorization": f"Bearer {sf_token}",
                           "Content-Type": "application/json"}
        self.sf_base = f"{sf_instance}/services/data/v62.0"
        self.d365_headers = {"Authorization": f"Bearer {d365_token}",
                             "Content-Type": "application/json",
                             "OData-MaxVersion": "4.0", "OData-Version": "4.0"}
        self.d365_base = f"https://{d365_env}.operations.dynamics.com/data"

    def sf_account_to_d365(self, account_id):
        acct = requests.get(
            f"{self.sf_base}/sobjects/Account/{account_id}",
            headers=self.sf_headers).json()
        d365_customer = {
            "CustomerAccount": acct.get("D365_Customer_Number__c") or "",
            "OrganizationName": acct["Name"][:100],
            "CustomerGroupId": "10",
            "dataAreaId": "usmf",
            "SalesCurrencyCode": acct.get("CurrencyIsoCode", "USD")}
        if d365_customer["CustomerAccount"]:
            key = f"CustomerAccount='{d365_customer['CustomerAccount']}',dataAreaId='usmf'"
            return requests.patch(f"{self.d365_base}/CustomersV3({key})",
                json=d365_customer, headers=self.d365_headers).json()
        else:
            resp = requests.post(f"{self.d365_base}/CustomersV3",
                json=d365_customer, headers=self.d365_headers).json()
            requests.patch(f"{self.sf_base}/sobjects/Account/{account_id}",
                json={"D365_Customer_Number__c": resp["CustomerAccount"]},
                headers=self.sf_headers)
            return resp

cURL: Quick API Tests

# Check Salesforce API limits remaining
curl -H "Authorization: Bearer ${SF_TOKEN}" \
  "${INSTANCE_URL}/services/data/v62.0/limits" \
  | jq '{DailyApiRequests, ConcurrentPerOrgDBConnections}'

# Check D365 F&O OData connectivity
curl -H "Authorization: Bearer ${D365_TOKEN}" \
  "https://${D365_ENV}.operations.dynamics.com/data/CustomersV3?\$top=1&\$select=CustomerAccount,OrganizationName"

# Salesforce: upsert Account via External ID
curl -X PATCH -H "Authorization: Bearer ${SF_TOKEN}" \
  -H "Content-Type: application/json" \
  "${INSTANCE_URL}/services/data/v62.0/sobjects/Account/D365_Customer_Number__c/CUST-0001" \
  -d '{"Name": "Acme Corp", "BillingCity": "New York"}'

Data Mapping

Field Mapping Reference

Source (Salesforce)Target (D365 F&O)TypeTransformGotcha
Account.NameCustomersV3.OrganizationNameStringTruncate to 100 charsD365 max 100 vs SF max 255
Account.BillingStreetCustomersV3.AddressStreetStringDirectD365 splits Street + StreetNumber; SF is single field
Account.BillingCountryCustomersV3.AddressCountryRegionIdStringMap name to ISO 3166-1D365 expects ISO code; SF may store full name
Opportunity.AmountSalesOrderHeadersV2.SalesOrderAmountCurrencyCurrency conversion if neededExchange rate timing: define which system's rate is authoritative
OpportunityLineItem.QuantitySalesOrderLines.SalesQuantityDecimalDirectSF allows 8 decimals; D365 default is 2
OpportunityLineItem.UnitPriceSalesOrderLines.SalesPriceCurrencyDirectTax-inclusive vs tax-exclusive depends on D365 tax group
Product2.ProductCodeSalesOrderLines.ItemNumberStringDirectMust match exactly; case-sensitive in D365
Account.Id (18-char)Custom field on CustTableStringDirectAlways use 18-char Salesforce ID (case-insensitive)

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

SourceCodeMeaningCauseResolution
D365 F&O429Too Many RequestsOData throttlingHonor Retry-After header; exponential backoff. For persistent 429s, switch to DMF
D365 F&O400Bad RequestInvalid payload, missing required fieldsParse InnerError.message; common: missing dataAreaId
D365 F&O404Not FoundEntity not OData-enabledVerify entity is public + OData-accessible in Data Management
D365 F&O401UnauthorizedToken expired or wrong audience URIVerify token audience matches environment URL (no trailing slash)
SalesforceINVALID_FIELDField doesn't existWrong API version or missing FLSCheck field-level security; verify API version
SalesforceDUPLICATE_VALUEExternal ID existsDuplicate record on upsertInvestigate: retry (safe) or legitimate duplicate?
SalesforceREQUEST_LIMIT_EXCEEDED24h API limit reachedToo many API callsReduce polling; switch to events; request limit increase

Failure Points in Production

Anti-Patterns

Wrong: Using Dual Write as a Salesforce-to-D365-F&O bridge

// WRONG -- Dual Write only connects D365 CE (Sales/Marketing/Service) to D365 F&O
// Architecture: Salesforce --> Dual Write --> D365 F&O  <-- DOES NOT WORK
// Salesforce --> Dataverse --> Dual Write --> D365 F&O  <-- Adds unnecessary hop

Correct: Direct middleware integration

// CORRECT -- Salesforce REST API --> Middleware --> D365 F&O OData
// Single integration layer, direct API-to-API mapping
// Salesforce Platform Event --> Azure Logic App --> D365 OData POST
// D365 Business Event --> Azure Service Bus --> Logic App --> SF REST PATCH

Wrong: Polling Salesforce for changes on a timer

# WRONG -- Polling every 5 min = 288 calls/day PER object
# 10 objects = 2,880 API calls/day just for polling
def poll_salesforce_accounts():
    accounts = sf_query("SELECT Id FROM Account WHERE LastModifiedDate > :last_run")
    for acct in accounts:
        sync_to_d365(acct)

Correct: Event-driven with Platform Events or CDC

# CORRECT -- Subscribe to Salesforce Change Data Capture
# Zero API calls wasted; instant notification on change
async def listen_for_changes():
    async with SalesforceStreamingClient(...) as client:
        await client.subscribe("/data/AccountChangeEvent")
        async for message in client:
            sync_to_d365(message["data"]["payload"])

Wrong: Sending records one-by-one to D365 for bulk loads

# WRONG -- 10,000 individual OData calls
for product in all_products:  # 10K products
    d365_odata_post("/data/ReleasedProductsV2", product)
    # Burns through 6,000/5min limit in under 3 minutes

Correct: Use Batch Data API (DMF) for bulk loads

# CORRECT -- Package data into DMF import (exempt from OData throttling)
csv_content = convert_to_csv(records)
response = requests.post(dmf_import_url,
    json={"packageUrl": upload_to_blob(csv_content),
          "definitionGroupId": "SFProductSync",
          "execute": True, "legalEntityId": "usmf"})

Common Pitfalls

Diagnostic Commands

# === Salesforce Diagnostics ===

# Check remaining API limits
curl -H "Authorization: Bearer ${SF_TOKEN}" \
  "${INSTANCE_URL}/services/data/v62.0/limits" \
  | jq '{DailyApiRequests, ConcurrentPerOrgDBConnections}'

# Verify integration user field permissions
curl -H "Authorization: Bearer ${SF_TOKEN}" \
  "${INSTANCE_URL}/services/data/v62.0/sobjects/Account/describe" \
  | jq '[.fields[] | select(.name | test("D365|Billing")) | {name, updateable}]'

# === D365 F&O Diagnostics ===

# Test OData connectivity
curl -H "Authorization: Bearer ${D365_TOKEN}" \
  "https://${D365_ENV}.operations.dynamics.com/data/CustomersV3?\$top=1&\$count=true"

# Check Azure Service Bus dead-letter queue depth
az servicebus queue show --resource-group ${RG} --namespace-name ${SB_NS} \
  --name ${QUEUE_NAME} --query "countDetails.deadLetterMessageCount"

Version History & Compatibility

Salesforce API Versions

API VersionReleaseStatusKey Changes
v62.0Spring '26 (Feb 2026)CurrentLatest GA
v61.0Winter '26 (Oct 2025)SupportedMinor CDC improvements
v60.0Spring '24 (Feb 2024)SupportedDeprecated legacy SOAP partner endpoints
v58.0Spring '23 (Feb 2023)SupportedAdded Client Credentials OAuth flow

D365 F&O Versions

VersionReleaseStatusKey Changes
10.0.412026 Wave 1 (Apr 2026)UpcomingEnhanced Business Events framework
10.0.402025 Wave 2 (Oct 2025)CurrentVirtual entity improvements
10.0.362024 Wave 2SupportedUser-based API limits disabled permanently

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Salesforce is your CRM and D365 F&O is your ERPBoth CRM and ERP are Microsoft (D365 CE + D365 F&O)D365 Dual Write
You need bidirectional Account/Customer, Opportunity-to-Order, or Invoice pushbackYou only need read-only D365 data in SalesforceSalesforce Connect External Objects (OData adapter)
Daily volume is <100K recordsDaily volume >1M records near-real-timeCustom ETL with D365 DMF + SF Bulk API 2.0
You have middleware budgetZero budget and zero dev resourcesPre-built connector (Rapidi, Celigo)

Cross-System Comparison

CapabilitySalesforce (API v62.0)D365 F&O (10.0.40+)Notes
API StyleREST + SOAP + BulkOData v4 + Custom Services + DMFBoth REST-based; OData more structured
Rate Limits100K+ calls/24h rolling6,000/5min per user per web serverD365 per-window not per-day
Bulk ImportBulk API 2.0 (150MB, async)DMF (package-based, async, exempt from throttling)Both async; D365 DMF more complex but no rate limit
Event-DrivenPlatform Events + CDC (72h replay)Business Events + Data Events (Azure transport)SF more mature; D365 requires Azure
Auth ModelOAuth 2.0 (JWT, Client Credentials)Azure AD/Entra ID OAuth 2.0SF self-contained; D365 depends on Azure AD
Data ModelObjects + Fields (flat, flexible)Data Entities + Tables (normalized, rigid)SF more flexible; D365 more structured
Metadata APIDescribe calls (per object)$metadata (full OData EDMX, 100MB+)Parse D365 metadata selectively

Important Caveats

Related Units