ORD-001 and ord-001 collide silently, causing 400 errors in production.This card compares duplicate detection and natural key management across five major ERP systems. Each system takes a fundamentally different approach: Salesforce and NetSuite provide first-class External ID fields with native upsert support, Microsoft Dynamics 365 enforces uniqueness through database-level alternate keys, SAP S/4HANA relies on fuzzy matching (ICM) for business partners and custom Z-fields for other entities, and Oracle ERP Cloud uses a source system reference model (OrigSystemReference) that supports multiple source system registrations per record.
| System | Role | Duplicate Detection Approach | Native Upsert? |
|---|---|---|---|
| Salesforce | CRM / Platform | ExternalId fields + Duplicate Rules | Yes — PATCH upsert by ExternalId |
| Oracle NetSuite | ERP / Financials | ExternalId + internalId | Yes — upsert/upsertList operations |
| SAP S/4HANA | ERP / Manufacturing | Fuzzy matching (ICM) + custom Z-fields | No native upsert — use BAPI + custom logic |
| Microsoft Dynamics 365 | CRM / ERP | Alternate Keys (DB index) + Duplicate Detection Rules | Yes — PATCH upsert by alternate key |
| Oracle ERP Cloud | ERP / Financials | OrigSystemReference + Source System Registration | Partial — FBDI merge, REST create-or-update |
Salesforce provides the most integration-friendly duplicate prevention. Custom fields can be marked as External ID (checkbox) and optionally Unique (checkbox). An External ID field that is also marked Unique creates a database-level unique index, enabling deterministic upsert. [src1]
How it works: The REST API upsert endpoint (PATCH /services/data/v62.0/sobjects/{Object}/{ExtIdField}/{ExtIdValue}) performs an atomic insert-or-update. If a record with the given External ID value exists, it updates; otherwise, it creates.
Duplicate Rules (separate feature): Configurable matching rules that fire on create/update via UI. By default, Duplicate Rules do NOT fire on API operations — you must explicitly set the Sforce-Duplicate-Rule-Header to false to enable them.
Limits: Max 7 External ID fields per standard object, 25 per custom object. External ID fields count toward the custom field limit.
NetSuite uses a single externalId attribute on most record types, set via SOAP web services (SuiteTalk). The upsert and upsertList operations match on externalId — if found, update; if not, create. [src2]
Critical constraint: Each record can have only ONE externalId value, and only one integrated application should own the externalId for a given record type.
Case sensitivity: External IDs in NetSuite are case-insensitive. CUST-001 and cust-001 are treated as the same value.
REST API: Use eid: prefix in URLs — e.g., GET /record/v1/customer/eid:CUST-001. [src8]
Not supported on: Accounting Periods, Budget Categories, Custom Field types, Custom Lists, Custom Record Types, Gift Certificates, Landed Costs, State records. SuiteScript does not support external IDs — only SOAP web services.
SAP S/4HANA has NO native External ID field for arbitrary record types. Duplicate detection varies by object type. [src3]
Business Partners (BP): Built-in fuzzy duplicate checking using Information Consistency Management (ICM). The system evaluates address data, name fields, and other attributes using configurable threshold values (0-100). A threshold of 90 means 90% data similarity triggers a duplicate flag.
Other entities: No built-in duplicate check. Integration teams must either create custom Z-fields (e.g., ZEXT_SYSTEM_ID) with custom ABAP duplicate checks, or implement deduplication in middleware.
BAPI considerations: BAPIs are not inherently idempotent. Calling BAPI_BUPA_CREATE_FROM_DATA twice with the same data creates two business partners. For idempotent creates, you must first call a search BAPI, then conditionally create or update.
Dynamics 365 (Dataverse) provides two distinct mechanisms. [src4]
Alternate Keys: Database-level unique indexes. Once defined, they enforce absolute uniqueness. Alternate keys enable upsert via the Web API: PATCH /api/data/v9.2/{entityset}({keyfield}='{value}'). [src6]
Duplicate Detection Rules: Application-level matching with configurable criteria. By default, these do NOT apply to Web API/SDK operations. To enable programmatically, set MSCRM.SuppressDuplicateDetection: false in the request header.
Key restrictions: Max 10 alternate keys per entity, max 5 fields per key. Only certain field types allowed: Decimal Number, Whole Number, Single Line of Text, DateTime, Lookup, Option Set. Special characters (/,<,>,*,%,&,:,\,?,+) in key field values break retrieve/update/upsert operations.
Oracle Fusion Cloud uses a source system reference model. Records are tracked through HZ_ORIG_SYS_REFERENCES, which maps source system names and source system reference IDs to internal Oracle party/object IDs. [src5]
Multiple source references: Unlike Salesforce or NetSuite, Oracle allows multiple source system references per record. A customer record can simultaneously have references from SAP, Salesforce, and a legacy system.
Source system registration: Each source system must be explicitly registered in Oracle Fusion before it can write source system references.
Merge behavior: When importing data with Allow Multiple Source System References enabled, Oracle can merge duplicate source records into a single Oracle record.
| Strategy | Example | Pros | Cons | Best For |
|---|---|---|---|---|
| Natural business key | PO-2026-00142, SKU-WIDGET-BLU | Human-readable, meaningful, stable across systems | May not be unique across systems, can change | Orders, Products, Invoices |
| GUID/UUID | f47ac10b-58cc-4372... | Globally unique, no coordination needed | Not human-readable, large (36 chars), no semantic meaning | Greenfield systems, event IDs |
| Composite key | SF:ACC-1001 (system + local ID) | Unique across systems, traceable to source | Requires consistent formatting | Cross-system mapping with 2 systems |
| Surrogate + mapping table | Internal integer ID + xref table | Decouples systems, handles N-to-N mappings | Additional infrastructure, lookup overhead | 3+ system integrations, MDM |
START — Choosing a cross-system ID strategy
├── How many systems are being integrated?
│ ├── 2 systems
│ │ ├── Does the target ERP have native External ID?
│ │ │ ├── YES (Salesforce, NetSuite, D365)
│ │ │ │ └── Use native ExternalId/alternate key with source system ID
│ │ │ └── NO (SAP, some Oracle scenarios)
│ │ │ └── Create custom field + mapping table in middleware
│ │ └── Is this bidirectional sync?
│ │ ├── YES → Each system stores the other's ID in its External ID field
│ │ └── NO → Source ID stored in target's External ID field
│ ├── 3+ systems
│ │ └── Build a cross-reference mapping table
│ │ ├── In middleware/iPaaS (MuleSoft xref, Boomi cross-reference)
│ │ └── OR in a dedicated mapping database
│ └── Hub-and-spoke (MDM)
│ └── Use Oracle OrigSystemReference pattern or MDM registry
├── Do records have a natural business key?
│ ├── YES (PO numbers, invoice numbers, SKUs)
│ │ └── Use the natural key as External ID value
│ └── NO (auto-generated IDs only)
│ └── Generate a composite key: {source_system}:{local_id}
└── Will records be created in multiple systems?
├── YES → Pre-assign IDs before any system creates the record (UUID)
└── NO → Use source system's native ID as the External ID in target
For integrations spanning 3+ systems, a dedicated cross-reference (xref) table is the most scalable approach. This pattern is used by Oracle SOA Suite, MuleSoft, Boomi, and most enterprise middleware platforms. [src7]
-- Cross-reference mapping table — one row per entity per system
CREATE TABLE integration_xref (
xref_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
entity_type VARCHAR(50) NOT NULL, -- 'customer', 'order', 'product'
canonical_id VARCHAR(100) NOT NULL, -- System-neutral ID (GUID or sequence)
source_system VARCHAR(30) NOT NULL, -- 'salesforce', 'netsuite', 'sap'
source_id VARCHAR(255) NOT NULL, -- ID in the source system
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
CONSTRAINT uq_xref_entity_system_id
UNIQUE (entity_type, source_system, source_id),
CONSTRAINT uq_xref_entity_canonical_system
UNIQUE (entity_type, canonical_id, source_system)
);
CREATE INDEX idx_xref_lookup
ON integration_xref (entity_type, source_system, source_id);
-- Translate Salesforce Account ID to NetSuite Customer ID
SELECT target.source_id AS netsuite_id
FROM integration_xref source
JOIN integration_xref target
ON source.entity_type = target.entity_type
AND source.canonical_id = target.canonical_id
WHERE source.entity_type = 'customer'
AND source.source_system = 'salesforce'
AND source.source_id = '001ABC123DEF'
AND target.source_system = 'netsuite'
AND target.is_deleted = FALSE;
| canonical_id | salesforce | netsuite | sap | d365 |
|---|---|---|---|---|
| CM-001 | 001ABC | 12345 | BP-0001 | {guid-1} |
| CM-002 | 001DEF | 12346 | BP-0002 | {guid-2} |
| CM-003 | 001GHI | null | BP-0003 | {guid-3} |
| Capability | Salesforce | NetSuite | SAP S/4HANA | Dynamics 365 | Oracle ERP Cloud |
|---|---|---|---|---|---|
| Native External ID field | Yes — custom field, checkbox | Yes — built-in attribute | No — use custom Z-field | No — use Alternate Keys | Yes — OrigSystemReference |
| Upsert operation | PATCH by ExternalId | upsert/upsertList (SOAP), eid: (REST) | No native upsert | PATCH by Alternate Key | FBDI merge / REST with source key |
| Uniqueness enforcement | DB unique index (if Unique checked) | DB unique per record type | Fuzzy threshold (ICM, BP only) | DB unique nonclustered index | Unique per source system + ref |
| Max external ID fields | 7/std, 25/custom | 1 per record (single value) | Unlimited custom Z-fields | 10 keys, 5 fields/key | Multiple source system refs |
| Case sensitivity | Case-sensitive | Case-insensitive | N/A (custom field dependent) | Case-sensitive | Case-sensitive |
| Multi-system support | 1 field per source system | 1 owner per record type | Custom design required | 1 key per source system | Multiple refs per record (native) |
| Duplicate rules (advisory) | Yes — configurable matching | Limited (saved search) | Yes — ICM fuzzy (BP) | Yes — configurable rules | Yes — CDM duplicate identification |
| API dedup default | OFF — opt-in via header | Always (via upsert) | ON for BP fuzzy check | OFF — opt-in via header | ON for source system refs |
| Bulk import dedup | External ID on Bulk API 2.0 | CSV import with externalId | Custom ABAP / middleware | DMF with alternate key | FBDI with source system key |
| Best for | CRM integration, multi-field matching | 2-system ERP sync | Master data (BP) dedup | Platform integrations | Hub-and-spoke MDM |
# Input: Account record with external system ID
# Output: Upserted record — created (201) or updated (204)
import requests # requests==2.32.3
def sf_upsert_by_external_id(instance_url, access_token, sobject, ext_id_field, ext_id_value, record_data):
url = f"{instance_url}/services/data/v62.0/sobjects/{sobject}/{ext_id_field}/{ext_id_value}"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
"Sforce-Duplicate-Rule-Header": "allowSave=true"
}
resp = requests.patch(url, json=record_data, headers=headers)
if resp.status_code == 201: # Created
return resp.status_code, resp.json().get("id")
elif resp.status_code == 204: # Updated (no content)
return resp.status_code, None
else:
raise Exception(f"Upsert failed: {resp.status_code} — {resp.text}")
[src1]
// Input: Customer record with externalId from source system
// Output: Upserted NetSuite customer — created or updated
const customer = {
recordType: 'customer',
externalId: externalId, // Source system ID — case-insensitive!
companyName: customerData.name,
email: customerData.email,
subsidiary: { internalId: '1' } // Required for OneWorld
};
const result = await client.upsert({ record: customer });
// REST alternative — use eid: prefix for external ID reference
// GET /record/v1/customer/eid:CUST-001 (retrieve)
// PATCH /record/v1/customer/eid:CUST-001 (update)
# Input: Contact record with alternate key (integration ID)
# Output: Upserted D365 contact — created or updated
def d365_upsert_by_alternate_key(org_url, access_token, entity_set, key_field, key_value, record_data):
url = f"{org_url}/api/data/v9.2/{entity_set}({key_field}='{key_value}')"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0"
}
resp = requests.patch(url, json=record_data, headers=headers)
if resp.status_code == 204: return "updated", None
elif resp.status_code == 201: return "created", resp.headers.get("OData-EntityId")
else: raise Exception(f"Upsert failed: {resp.status_code} — {resp.text}")
# Step 1: Search for existing Business Partners matching the name
curl -X GET \
"https://my-s4hana.com/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner?\$filter=BusinessPartnerFullName eq 'Acme Corporation'" \
-H "Authorization: Basic $(echo -n 'user:pass' | base64)" \
-H "Accept: application/json"
# Step 2: If no match found, create new BP with custom Z-field for external reference
curl -X POST \
"https://my-s4hana.com/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner" \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: <fetched-token>" \
-d '{"BusinessPartnerCategory":"2","BusinessPartnerFullName":"Acme Corporation","YY1_ExtSystemID_bus":"SF:001ABC"}'
# Note: SAP has no native upsert — you MUST search first, then create or update.
[src3]
| Code/Error | System | Meaning | Cause | Resolution |
|---|---|---|---|---|
DUPLICATE_VALUE | Salesforce | Unique field constraint violated | Two records have same External ID value | Merge duplicates or fix source data |
DUPLICATE_EXTERNAL_ID | Salesforce | Multiple source records share same External ID | Batch upsert file has duplicate keys | Deduplicate source file pre-submission |
| 400 Bad Request | NetSuite | ExternalId collision | Attempting to set externalId to value already assigned to another record | Query existing record first, update instead |
0x80040237 | Dynamics 365 | Alternate key violation | Record with same key values already exists | Use upsert (PATCH) instead of create (POST) |
DUPLICATE_DETECTED | Dynamics 365 | Duplicate Detection Rule triggered | SuppressDuplicateDetection: false header set and match found | Merge records or set header to true |
Subscribe to merge events and propagate the mapping update to all connected systems. [src1]ORD-001 and ord-001 as different records; NetSuite treats them as the same. Fix: Normalize all external IDs to uppercase before sending to NetSuite. [src2]Implement pre-check search in middleware before BAPI create call. [src3]/,<,>,*,%,&,:,\,?,+, the upsert returns 400. Fix: URL-encode values OR use a sanitized ID format (alphanumeric + hyphens only). [src4]Use surrogate keys (GUID/sequence) as canonical IDs — never use mutable business attributes as integration keys. [src7]# BAD — Using Salesforce auto-generated record ID as the primary integration key
# Salesforce IDs are org-specific — they change between sandboxes,
# are meaningless to other systems, and cannot be pre-assigned.
new_order = {
"sfdc_id": "a0B5e00000XYZ123", # Salesforce auto-ID
"netsuite_id": None # Race condition if NS create fails
}
# GOOD — Pre-assign a canonical ID before creating in ANY system.
import uuid
canonical_id = str(uuid.uuid4())
sf_external_id = f"INT:{canonical_id}"
# Both systems receive the same cross-reference on first creation.
# Retry-safe and traceable.
// BAD — Passing External IDs without normalization
const sourceId = "Acct-ABC-123"; // From Salesforce
// NetSuite treats "Acct-ABC-123" and "acct-abc-123" as the SAME record
// GOOD — Always normalize to consistent case before using as External ID
function normalizeExternalId(systemId, targetSystem) {
if (targetSystem === 'netsuite') return systemId.toUpperCase();
return systemId.trim();
}
# BAD — Reusing NetSuite's single externalId for multiple integration partners
# System A writes externalId = "SYS_A:001"
# System B overwrites externalId = "SYS_B:XYZ"
# System A's reference is now broken.
# GOOD — Primary system uses externalId, others use custom fields
netsuite_record = {
"externalId": "SALESFORCE:001ABC", # Primary partner
"custentity_sap_bp_id": "BP-0001", # Custom field for SAP
"custentity_legacy_crm_id": "C-3003" # Custom field for legacy
}
Parse the result file, retry only failed records using External ID upsert. [src1]Prefix external IDs with the record type: 'CUST:EMP-001' vs 'EMP:EMP-001'. [src2]Define an alternate key for the integration key field; use duplicate detection rules only for UI-based data entry guidance. [src4]Register all source systems in Oracle Fusion Setup before running integrations. [src5]Use immutable identifiers (auto-generated IDs, GUIDs) as integration keys. Store natural keys as searchable attributes, not as the integration key. [src7]# Salesforce: Check External ID field configuration
curl -s "$SF_INSTANCE/services/data/v62.0/sobjects/Account/describe" \
-H "Authorization: Bearer $SF_TOKEN" | \
jq '.fields[] | select(.externalId==true) | {name, label, unique, type}'
# NetSuite: Retrieve record by External ID (REST)
curl -s "https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/record/v1/customer/eid:CUST-001" \
-H "Authorization: OAuth ..."
# Dynamics 365: List alternate keys on an entity
curl -s "$D365_URL/api/data/v9.2/EntityDefinitions(LogicalName='contact')/Keys" \
-H "Authorization: Bearer $D365_TOKEN" \
-H "OData-Version: 4.0"
# Cross-reference table: Find orphaned mappings
# SELECT x.* FROM integration_xref x
# LEFT JOIN salesforce_accounts sf ON x.source_id = sf.id
# WHERE sf.id IS NULL AND x.source_system = 'salesforce' AND x.is_deleted = FALSE;
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building any multi-system integration that creates or syncs records | One-time data migration (load and done) | Pre-migration cleansing + dedup tools |
| Need deterministic insert-or-update behavior across systems | Single-system CRUD with no external references | Native platform duplicate rules only |
| Designing ID strategy for 3+ system integration | Systems only share read-only data (no writes) | Simple API queries — no dedup needed |
| Implementing real-time bidirectional sync | Event-driven read replicas with no writeback | Change Data Capture without dedup |
| MDM hub needs to track record provenance across sources | Already using a commercial MDM product | Vendor MDM (Informatica MDM, Reltio, Semarchy) |