Design reliable integrations between ERP systems and the rest of your technology landscape. Covers middleware patterns, API vs file-based integration, idempotency, error handling, master data synchronization, and the patterns that keep ERP integrations stable.
ERP integration architecture defines the structure, protocols, and operational patterns for connecting an enterprise resource planning (ERP) system with other business systems—such as CRM, HRIS, e-commerce, supply chain, and analytics platforms—through consistent, reliable, and scalable data exchange. This architecture matters most during system migrations, when new business capabilities are introduced, or when an organization scales beyond a single ERP instance. It is the backbone of real-time, bidirectional, and event-driven data flows across enterprise boundaries.
When immediate feedback is critical—such as confirming a sales order in ERP before completing a checkout—synchronous HTTP APIs are the foundation. The ERP exposes endpoints using standard REST principles: JSON payloads, HTTP status codes, and request/response headers.
A typical integration request uses POST /api/salesorders with a JSON body:
{
"customerNumber": "CUST-10042",
"orderDate": "2023-10-15",
"items": [
{
"productId": "PROD-001",
"quantity": 5,
"unitPrice": 29.99
}
]
}
The ERP responds with a 201 Created status and a Location header pointing to the new order:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/salesorders/12345
{
"orderId": "12345",
"status": "Confirmed",
"totalAmount": 149.95,
"createdAt": "2023-10-15T10:30:00Z"
}
Common failures include:
validationErrors array: {
"validationErrors": [
{
"field": "customerNumber",
"message": "Customer number must be at least 6 characters long"
},
{
"field": "items[0].quantity",
"message": "Quantity must be a positive integer"
}
]
}
The integration must handle partial success: when one item fails but the order is created, the ERP returns 207 Multi-Status, with a body structured as:
{
"statusCode": 207,
"multiStatus": [
{
"statusCode": 201,
"resource": "/api/salesorders/12345",
"headers": { "ETag": "W/\"abc123\"" },
"body": { "orderId": "12345", "status": "Confirmed" }
},
{
"statusCode": 422,
"resource": "/api/salesorders/12345/items/0",
"body": {
"field": "quantity",
"message": "Quantity must be greater than 0"
}
}
]
}
When latency is acceptable and system availability is key—such as updating inventory after a warehouse receipt—message queues are used. The ERP publishes events to a message broker like Apache Kafka or RabbitMQ.
A InventoryUpdated event is published as a JSON message on a topic named inventory.events:
{
"eventId": "evt-88765",
"eventType": "InventoryUpdated",
"timestamp": "2023-10-15T11:22:33Z",
"data": {
"productId": "PROD-001",
"locationCode": "WH-01",
"quantity": 100,
"sourceSystem": "Warehouse Management System"
}
}
Consumers (e.g., an e-commerce platform, a reporting service) subscribe to this topic and process messages in batches or individually. The message broker guarantees delivery through acknowledgments (acks) and retry mechanisms.
The message format includes metadata in the message headers:
Content-Type: application/json
X-Message-Id: evt-88765
X-Source-System: ERP
X-Event-Version: 1.1
Failure modes:
messageId to deduplicate.queueDepth and processingLatency metrics.For high-volume, low-frequency data transfers—like nightly sales reports or monthly financial consolidations—batch file integrations dominate. The ERP generates a flat file (CSV, XML, or EDI) and places it in a shared directory or cloud storage.
A daily sales summary file is generated as sales-20231015.csv:
orderNumber,customerNumber,orderDate,productCode,quantity,unitPrice,totalAmount
SO-12345,CUST-10042,2023-10-15,PROD-001,5,29.99,149.95
SO-12346,CUST-10043,2023-10-15,PROD-002,3,15.50,46.50
The file is written to /exports/sales/daily/20231015.csv and then moved to a completed subdirectory. A separate process—running via systemctl—monitors the exports directory:
systemctl status erp-batch-import
# ● erp-batch-import.service - ERP Batch Import Daemon
# Loaded: loaded (/etc/systemd/system/erp-batch-import.service; enabled; vendor preset: enabled)
# Active: active (running) since Tue 2023-10-15 02:00:00 UTC; 1h 32min ago
# Main PID: 1245 (importer)
# Status: "Processing 48 files in queue"
# Tasks: 2 (limit: 4915)
# Memory: 145.2M
# CGroup: /system.slice/erp-batch-import.service
# └─1245 /usr/bin/erp-importer --config /etc/erp-import.conf
The erp-import.conf file defines:
[batch]
directory = "/exports/sales/daily"
completed = "/exports/sales/completed"
error = "/exports/sales/errors"
format = "csv"
delimiter = ","
encoding = "utf-8"
schema = [
{ field = "orderNumber", type = "string", required = true },
{ field = "customerNumber", type = "string", required = true },
{ field = "orderDate", type = "date", format = "yyyy-MM-dd" },
{ field = "productCode", type = "string" },
{ field = "quantity", type = "integer", min = 1 },
{ field = "unitPrice", type = "decimal", scale = 2 },
{ field = "totalAmount", type = "decimal", scale = 2 }
]
[logging]
level = "info"
file = "/var/log/erp-importer.log"
max-size = "100MB"
max-backups = 10
Failures in batch processing:
ERROR: Failed to parse row 123 in sales-20231015.csv
Field: unitPrice
Value: "29,99"
Expected: decimal
Error: Invalid decimal format: expected '12.34', got '12,34'
completed/. The importer processes the same file multiple times. A lastProcessed timestamp file is used to track processed files.When the ERP must propagate changes in real time—such as when a purchase order is approved—the architecture leverages change data capture (CDC). The ERP logs changes to a database table (ChangeLog) and triggers events based on insertions, updates, or deletions.
A PurchaseOrder is updated:
INSERT INTO ChangeLog (entity, entityId, operation, timestamp, data)
VALUES (
'PurchaseOrder',
'PO-2023-1001',
'UPDATE',
'2023-10-15 13:45:12',
'{"status": "Approved", "approver": "[email protected]"}'
);
A service listens for new entries in ChangeLog and emits events via a message broker. The event is consumed by downstream systems.
The service uses a cursor-based CDC approach:
SELECT id, entity, entityId, operation, data, timestamp
FROM ChangeLog
WHERE id > ?lastProcessedId
ORDER BY id
LIMIT 1000;
The service stores the last processed id in a cdc_checkpoint table:
CREATE TABLE cdc_checkpoint (
source_table VARCHAR(64) PRIMARY KEY,
last_id BIGINT NOT NULL,
last_timestamp TIMESTAMP WITH TIME ZONE
);
A failure occurs when the service crashes after processing 10,000 events but before updating the checkpoint. On restart, it resumes from last_id, avoiding duplicate processing.
When a system outside the ERP needs to react to events—such as a CRM updating a customer record—the ERP exposes webhooks. A client registers a URL to receive notifications:
POST /api/webhooks
{
"topic": "customer.updated",
"url": "https://crm.example.com/webhook/customer-updated",
"events": [
"customer.created",
"customer.updated",
"customer.deleted"
],
"filter": {
"tenantId": "tenant-123"
}
}
The ERP sends POST requests to the registered URL whenever a matching event occurs. The payload is a standard JSON object with metadata:
{
"id": "webhook-78912",
"topic": "customer.updated",
"timestamp": "2023-10-15T14:00:00Z",
"data": {
"customerId": "CUST-10042",
"name": "Acme Corp",
"email": "[email protected]",
"lastUpdatedBy": "[email protected]"
},
"context": {
"tenantId": "tenant-123",
"sourceSystem": "ERP"
}
}
The webhook receiver must:
202 Accepted500 Internal Server Error if the endpoint is downX-Webhook-Id and X-Webhook-Timestamp to deduplicate messagesIntegrations fail silently. A successful response does not imply correctness. The architecture must include:
{
"timestamp": "2023-10-15T14:01:02Z",
"integration": "sales-order-to-erp",
"step": "send-order",
"status": "failed",
"error": {
"code": "ERP-503",
"message": "ERP service unavailable",
"details": "Connection timeout after 10 seconds"
},
"context": {
"orderId": "SO-12345",
"externalId": "ext-12345"
}
}
integration_queue table with retry attempts and backoff schedules: CREATE TABLE integration_queue (
id BIGSERIAL PRIMARY KEY,
integration_name VARCHAR(128),
payload JSONB,
status VARCHAR(20) DEFAULT 'pending',
attempts INT DEFAULT 0,
next_retry TIMESTAMP WITH TIME ZONE,
error_log TEXT,
created_at TIMESTAMP WITH TIME ZONE
);
check-integration-health script runs every 5 minutes and reports: #!/bin/bash
# check-integration-health
set -euo pipefail
for integration in sales-order-to-erp inventory-sync crm-customer-updates; do
last_run=$(psql -c "SELECT max(created_at) FROM integration_queue WHERE integration_name = '$integration' AND status = 'completed'" -t)
if [ -z "$last_run" ] || [ "$(date -d "$last_run" +%s)" -lt $(( $(date +%s) - 300 )) ]; then
echo "WARNING: $integration has not completed in the last 5 minutes"
fi
done
POST /api/salesorders must include a X-Request-Id header to deduplicate requests. The ERP returns 200 OK if the order already exists and 201 Created if it’s new.Authorization: Bearer <token>. Token format is JWT, with claims like iss, aud, exp, and sub.Accept: application/vnd.erp.v1+json and Content-Type: application/vnd.erp.v1+json.errors directory contains failed-20231015.csv with detailed logs.This architecture is not a set of tools but a set of expectations: reliable, observable, resilient, and predictable data flow between systems. It is the foundation upon which enterprise software operates.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy