This playbook covers end-to-end integration for connecting any major ERP system (SAP S/4HANA, Oracle NetSuite, Microsoft Dynamics 365) with a PIM system (Akeneo, Salsify, inRiver, or Stibo STEP). The ERP acts as the operational product master; the PIM serves as the commercial content enrichment and channel syndication layer. Middleware or iPaaS platforms typically orchestrate the data flow. [src2, src6]
| System | Role | API Surface | Direction |
|---|---|---|---|
| ERP (SAP, NetSuite, D365) | Operational product master — SKU, cost, weight, dims, compliance | OData v4, REST, SOAP, SuiteTalk | Outbound (ERP -> PIM) |
| PIM (Akeneo, Salsify, inRiver, Stibo) | Content enrichment hub — descriptions, media, translations | REST, GraphQL, Webhooks | Inbound + Outbound |
| DAM (Cloudinary, Bynder, Widen) | Media asset management — images, videos, 3D models | REST | Bidirectional with PIM |
| iPaaS / Middleware | Integration orchestration — mapping, transformation, error handling | N/A | Orchestrator |
| eCommerce / Marketplace | Sales channels — Shopify, Magento, Amazon, eBay | REST, GraphQL | Inbound (PIM -> channel) |
| PIM System | API Surface | Protocol | Best For | Bulk Import | Webhook Support | Event-Driven? |
|---|---|---|---|---|---|---|
| Akeneo PIM | REST API v1 | HTTPS/JSON | Product CRUD, attribute management | 100/batch PATCH | Event Platform (Enterprise+) | Yes |
| Akeneo PIM | GraphQL API | HTTPS/GraphQL | Complex product queries | No | N/A | No |
| Salsify PXM | REST API | HTTPS/JSON | Product CRUD, digital assets | 50K/job | Yes | Yes |
| Salsify PXM | GraphQL API | HTTPS/GraphQL | Complex queries, nested relationships | No | N/A | No |
| Salsify PXM | SFTP | SFTP/CSV | Legacy bulk import, scheduled exports | Unlimited (chunked at 50K) | N/A | No |
| inRiver iPMC | REST API | HTTPS/JSON | Entity CRUD, link management | 500/batch | Yes | Yes |
| Stibo STEP | REST API | HTTPS/JSON | Product CRUD, workflows | STEPXML (no limit) | Yes | Yes |
| Stibo STEP | STEPXML | XML/HTTPS | Bulk import/export, migration | Unlimited | N/A | No |
| PIM System | Limit Type | Value | Notes |
|---|---|---|---|
| Akeneo | Products per PATCH batch | 100 | Use /api/rest/v1/products with JSON array |
| Akeneo | Request body size | 50 MB | Applies to product import payloads |
| Akeneo | Media file size | 100 MB per asset | Larger assets require chunked upload |
| Salsify | Products per bulk import job | 50,000 | Auto-chunks internally |
| Salsify | API response page size | 250 records | Cursor-based pagination |
| inRiver | Entities per batch write | 500 | Batch entity creation endpoint |
| Stibo STEP | STEPXML import file size | No hard limit | Practical limit ~500MB |
| PIM System | Limit Type | Value | Window | Edition Differences |
|---|---|---|---|---|
| Akeneo (SaaS) | API calls | Managed throttling | Rolling | Serenity: higher burst; Growth: lower concurrency |
| Akeneo (on-prem) | API calls | Configurable | N/A | No vendor-imposed limit |
| Salsify | API calls | Fair use ~100-300 req/min | Per minute | 429 + Retry-After header |
| inRiver | API calls | Fair use — throttled per tenant | Per minute | Enterprise: higher concurrency |
| Stibo STEP | API calls | Configurable | N/A | On-premise: unlimited; SaaS: managed |
| PIM System | Auth Method | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|---|
| Akeneo | OAuth 2.0 Client Credentials | Server-to-server integration | 1 hour | Yes | Generate in PIM > Connections |
| Salsify | API Key + Org ID | All integrations | No expiry | N/A | Bearer token; scoped per user |
| inRiver | API Key | All integrations | No expiry | N/A | X-inRiver-APIKey header |
| Stibo STEP | OAuth 2.0 / SAML | Cloud or hybrid | Session-based | Yes | On-prem supports basic auth |
START — Integrate ERP with PIM for product data enrichment
├── What's the data flow?
│ ├── ERP -> PIM (product master seeding)
│ │ ├── Catalog < 10K SKUs? -> REST API batch import
│ │ ├── Catalog < 100K SKUs? -> Bulk API or SFTP
│ │ └── Catalog > 100K SKUs? -> File-based + chunked (50K batches)
│ ├── PIM -> Channels (enrichment -> syndication)
│ │ ├── Channel supports API? -> PIM native connector or iPaaS
│ │ └── No API? -> PIM scheduled export -> SFTP/file drop
│ └── Bidirectional (ERP <-> PIM)
│ ├── Define field-level ownership rules FIRST
│ └── Implement conflict resolution before any sync
├── Which middleware?
│ ├── SAP ERP -> Akeneo: Akeneo Accelerators on SAP BTP
│ ├── NetSuite -> Any PIM: Celigo or Boomi
│ ├── D365 -> Any PIM: Azure Integration Services or MuleSoft
│ └── Generic -> Any PIM: Workato, MuleSoft, or Boomi
└── Error handling?
├── Zero-loss -> idempotent ops + dead letter queue + reconciliation
└── Best-effort -> retry with backoff + daily reconciliation
| Step | Source | Action | Target | Data Objects | Failure Handling |
|---|---|---|---|---|---|
| 1 | ERP | Export product master data (SKU, cost, weight, dims) | iPaaS | Item master records | Retry 3x, then DLQ |
| 2 | iPaaS | Transform: map ERP fields to PIM attribute families | PIM | Product entities | Validation errors -> error log |
| 3 | PIM | Enrichment: descriptions, media, translations, SEO | PIM (internal) | Enriched products | Completeness workflow notifications |
| 4 | PIM | Completeness gate passed -> ready for channel | PIM (internal) | Channel-ready products | Incomplete -> held in workflow |
| 5 | PIM | Syndication: export channel-specific product feed | eCommerce / Marketplace | Channel-formatted catalog | Per-channel error logs; retry |
| 6 | ERP | Price/inventory update (bypasses PIM) | eCommerce | Pricing + stock | Direct ERP-to-channel |
| 7 | eCommerce | Order placed (triggers O2C flow) | ERP | Sales order | Separate O2C integration |
| Capability | Akeneo PIM | Salsify PXM | inRiver iPMC | Stibo STEP |
|---|---|---|---|---|
| API Style | REST + GraphQL | REST + GraphQL | REST | REST + STEPXML + SOAP |
| Bulk Import | 100/batch via API | 50K/job; unlimited SFTP | 500/batch | STEPXML (no limit) |
| Event-Driven | Event Platform (Enterprise+) | Webhooks (all editions) | Webhooks (all editions) | Outbound events + workflows |
| Authentication | OAuth 2.0 | API Key + Org ID | API Key | OAuth 2.0 / SAML |
| DAM Integration | Built-in basic + connectors | Full built-in DAM | Full built-in DAM | Connectors only |
| Channel Syndication | Via connectors | Built-in engine (strongest) | Built-in feeds | Via integrations |
| SAP Connector | Accelerators on SAP BTP | Partner connectors | Standard connector | Dedicated connector |
| Open Source | Community Edition (free) | No | No | No |
| Best For | Developer-friendly; open ecosystem | Brand manufacturers; retail/marketplace | B2B manufacturers; complex products | Enterprises needing MDM + PIM |
Create a field-by-field ownership matrix specifying which system is authoritative for each attribute. This prevents the #1 PIM integration failure: two systems overwriting each other's data. [src1, src2]
DATA OWNERSHIP MATRIX (example)
| Attribute | Owner | Source System | Consumers |
|--------------------|-------|---------------|---------------------|
| SKU / Item Number | ERP | SAP S/4HANA | PIM, eCommerce |
| Cost Price | ERP | SAP S/4HANA | Internal only |
| Sell Price | ERP | SAP S/4HANA | eCommerce (direct) |
| Product Name | PIM | Akeneo | eCommerce, marketplace |
| Description (EN) | PIM | Akeneo | eCommerce, marketplace |
| Hero Image | DAM | Cloudinary | PIM, eCommerce |
Verify: Every field has exactly ONE owner. No field is blank in the "Owner" column.
Each PIM system groups attributes into families (Akeneo), schemas (Salsify), or entity types (inRiver). Map each ERP product category to the corresponding PIM family. [src1, src5]
// Akeneo: Create product family via REST API
// POST /api/rest/v1/families
{
"code": "electronics_accessories",
"attributes": ["sku", "name", "description", "hero_image", "weight", "ean"],
"attribute_as_label": "name",
"attribute_requirements": {
"ecommerce": ["sku", "name", "description", "hero_image", "ean"],
"amazon": ["sku", "name", "amazon_bullet_points", "hero_image", "ean"]
}
}
Verify: GET /api/rest/v1/families/electronics_accessories returns the family with all attributes.
Set up the ERP to export product master data to middleware/iPaaS. Use change-based triggers where possible. [src2]
# SAP S/4HANA OData product export (delta pattern)
import requests
params = {
"$filter": f"LastChangeDateTime gt datetime'{last_sync}'",
"$select": "Product,ProductDescription,GrossWeight,WeightUnit,ProductGroup",
"$top": 1000
}
response = requests.get(f"{SAP_BASE}/Product", headers=headers, params=params)
products = response.json().get("value", [])
Verify: Response status 200; value array contains product records.
Map ERP fields to PIM attributes, convert data types, and batch-import into PIM. [src1]
# Akeneo batch import (max 100 products per PATCH)
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/vnd.akeneo.collection+json"}
payload = "\n".join(json.dumps(transform(p)) for p in products[:100])
resp = requests.patch(f"{AKENEO_BASE}/api/rest/v1/products", headers=headers, data=payload)
# Parse per-product status: 201=created, 204=updated, 422=validation error
Verify: Response contains per-product status lines; no 422 errors.
Set up PIM completeness rules and channel-specific exports. [src8]
# Salsify: trigger channel export
export_resp = requests.post(f"{SALSIFY_BASE}/orgs/{ORG_ID}/channel_runs",
headers=headers, json={"channel_id": SHOPIFY_CHANNEL_ID})
# Poll for completion, check products_exported count
Verify: Channel run "completed"; product count matches expected; check channel for missing products.
Set up incremental sync and periodic full reconciliation to catch drift. [src2]
# Delta sync: only changed products since last run
changed = erp.get_products(modified_after=last_sync_timestamp)
pim.batch_import(changed)
# Weekly: full reconciliation — compare ERP vs PIM product counts
Verify: Delta sync updates only changed products; reconciliation report shows zero drift.
# Input: ERP product records (list of dicts), Akeneo OAuth credentials
# Output: Sync report with created/updated/failed counts
import requests, json
class AkeneoPIMSync:
def __init__(self, base_url, client_id, client_secret):
self.base_url = base_url
self.client_id = client_id
self.client_secret = client_secret
self.token = None
def authenticate(self):
resp = requests.post(f"{self.base_url}/api/oauth/v1/token", json={
"grant_type": "client_credentials",
"client_id": self.client_id, "client_secret": self.client_secret
})
resp.raise_for_status()
self.token = resp.json()["access_token"]
def batch_upsert(self, products, batch_size=100):
headers = {"Authorization": f"Bearer {self.token}",
"Content-Type": "application/vnd.akeneo.collection+json"}
results = {"created": 0, "updated": 0, "failed": 0, "errors": []}
for i in range(0, len(products), batch_size):
batch = products[i:i + batch_size]
payload = "\n".join(json.dumps(p) for p in batch)
resp = requests.patch(f"{self.base_url}/api/rest/v1/products",
headers=headers, data=payload)
for line in resp.text.strip().split("\n"):
result = json.loads(line)
code = result.get("status_code")
if code == 201: results["created"] += 1
elif code == 204: results["updated"] += 1
else:
results["failed"] += 1
results["errors"].append(result)
return results
// Input: ERP product records, inRiver API key
// Output: Created entity IDs
const axios = require('axios'); // [email protected]
const INRIVER_BASE = 'https://apieuw.productmarketingcloud.com/api/v1.0.0';
async function importToInRiver(products, apiKey) {
const headers = { 'X-inRiver-APIKey': apiKey, 'Content-Type': 'application/json' };
const results = { created: 0, failed: 0, errors: [] };
const batchSize = 500;
for (let i = 0; i < products.length; i += batchSize) {
const batch = products.slice(i, i + batchSize).map(p => ({
entityTypeId: 'Product',
fieldValues: [
{ fieldTypeId: 'ProductNumber', value: p.sku },
{ fieldTypeId: 'ProductName', value: p.name }
]
}));
try {
const resp = await axios.post(`${INRIVER_BASE}/entities:createmany`, batch, { headers });
results.created += resp.data.length;
} catch (err) {
results.failed += batch.length;
results.errors.push(err.response?.data || err.message);
}
}
return results;
}
# Get OAuth token
curl -X POST "https://your-akeneo.cloud.akeneo.com/api/oauth/v1/token" \
-H "Content-Type: application/json" \
-d '{"grant_type":"client_credentials","client_id":"ID","client_secret":"SECRET"}'
# Expected: {"access_token":"...","expires_in":3600}
# List products
curl "https://your-akeneo.cloud.akeneo.com/api/rest/v1/products?limit=10" \
-H "Authorization: Bearer TOKEN"
# Expected: {"_embedded":{"items":[...]}}
| ERP Field (SAP S/4HANA) | PIM Field (Akeneo) | Type | Transform | Gotcha |
|---|---|---|---|---|
| MATNR (Material Number) | identifier (SKU) | String | Strip leading zeros | SAP pads to 18 chars; PIM excludes padding |
| MAKTX (Material Description) | name (localizable) | String | Map per locale | ERP description is internal, not customer-facing |
| BRGEW (Gross Weight) | weight (metric) | Decimal + Unit | Convert SAP KG -> Akeneo KILOGRAM | SAP stores 3 decimal places; PIM may differ |
| MEINS (Base UoM) | base_unit | Enum | Map UoM codes | SAP has 500+ codes; PIM supports 20-50 |
| MATKL (Material Group) | family + categories | Mapping table | Lookup table required | Hardest mapping — requires business input |
| EAN11 (GTIN/EAN) | ean (global) | String | Validate check digit | Filter placeholder GTINs (all zeros) |
| PRDHA (Product Hierarchy) | categories (multi-value) | String -> Array | Split hierarchy codes | SAP uses 18-char concatenated; PIM uses tree |
| FERTH (Production/Batch) | N/A — skip | N/A | Do not sync | Internal manufacturing data |
{"amount": "1.500", "unit": "KILOGRAM"} objects. Bare numbers cause validation errors. [src1]| Code | PIM System | Meaning | Cause | Resolution |
|---|---|---|---|---|
| 401 | All | Unauthorized | Expired token or revoked key | Refresh OAuth; verify API key active |
| 422 | Akeneo | Unprocessable entity | Invalid attribute, missing required field | Check error message; validate against family |
| 429 | All | Rate limit exceeded | Too many API calls | Exponential backoff; respect Retry-After |
| 404 | All | Entity not found | Referencing non-existent product/family | Create prerequisites first |
| 400 | Salsify | Bad request | Malformed JSON, invalid property | Validate payload against schema |
| 409 | inRiver | Conflict | Concurrent update to same entity | Retry with jitter; optimistic locking |
Create catch-all family; route unmapped products to classification queue. [src1]Monitor PIM vs channel product count delta; alert on >5% gap. [src8]Compress to max 5MB; use async upload; prefer CDN URLs over binary upload. [src1]Token cache with mutex; or separate credentials per worker. [src1]Force UTF-8 in middleware transformation; validate encoding before API call. [src2]Store mappings in middleware config; alert on unmapped categories; webhook on category changes. [src5]# BAD — routing price updates through PIM adds 15-60 min latency
ERP (price change) -> iPaaS -> PIM -> iPaaS -> eCommerce
# Customers see stale pricing
# GOOD — hot data bypasses PIM entirely
ERP (price change) -> iPaaS -> eCommerce (direct, <1 min)
ERP (product master) -> iPaaS -> PIM (cold data only)
PIM (enriched content) -> iPaaS -> eCommerce
# BAD — scanning all 500K products every hour
all_products = erp.get_all_products() # 500K records, 45 min
pim.batch_import(all_products) # 5,000 API calls, 2 hours
# GOOD — only sync changed products
changed = erp.get_products(modified_after=last_sync) # ~50 records
pim.batch_import(changed) # 1 API call
# BAD — 50MB payload, timeout, 413 Payload Too Large
product["hero_image"] = base64_encode(read_file("product.jpg"))
# GOOD — upload asset first, reference in product
media_resp = requests.post(f"{AKENEO}/api/rest/v1/media-files", files=files)
product["hero_image"] = media_resp.headers["Location"].split("/")[-1]
Build mapping table first; reject unmapped products to review queue. [src1]ERP-to-ecommerce direct for hot data. [src1, src2]Define completeness rules per channel; build pre-syndication validation. [src8]Version field mapping config; detect model changes via API; alert on new required attributes. [src5]Always specify locale and scope; validate against attribute properties. [src1]Build two flows — initial load (SFTP/STEPXML, overnight) and delta sync (API, event-driven). [src2]# Test Akeneo OAuth authentication
curl -s -o /dev/null -w "%{http_code}" \
-X POST "https://your-akeneo.cloud.akeneo.com/api/oauth/v1/token" \
-H "Content-Type: application/json" \
-d '{"grant_type":"client_credentials","client_id":"ID","client_secret":"SECRET"}'
# Expected: 200
# Check Akeneo product count
curl -s "https://your-akeneo.cloud.akeneo.com/api/rest/v1/products?limit=1" \
-H "Authorization: Bearer TOKEN" | python -c "import sys,json; print(json.load(sys.stdin).get('items_count'))"
# List Akeneo families
curl -s "https://your-akeneo.cloud.akeneo.com/api/rest/v1/families" \
-H "Authorization: Bearer TOKEN"
# Test Salsify API key
curl -s -o /dev/null -w "%{http_code}" \
"https://app.salsify.com/api/v1/orgs/ORG_ID/products?per_page=1" \
-H "Authorization: Bearer API_KEY"
# Expected: 200
# Test inRiver API key
curl -s -o /dev/null -w "%{http_code}" \
"https://apieuw.productmarketingcloud.com/api/v1.0.0/model/entitytypes" \
-H "X-inRiver-APIKey: YOUR_API_KEY"
# Expected: 200
| System | Version | Release | Status | Breaking Changes | Notes |
|---|---|---|---|---|---|
| Akeneo PIM | v7.0 | 2024-03 | Current | Event Platform GA; new permissions | GraphQL expanded |
| Akeneo PIM | v6.0 | 2023-03 | Supported | Asset Manager replaced PAM | Migrate from PAM |
| Akeneo PIM | v5.0 | 2022-01 | EOL | New category tree API | Minimum for Event Platform |
| Salsify PXM | 2025 | 2025-01 | Current | GraphQL API GA | Enhanced bulk ops |
| inRiver iPMC | 2025 | 2025-01 | Current | None | Enhanced REST endpoints |
| Stibo STEP | 2025 | 2025-01 | Current | REST API v2 | Legacy SOAP deprecated |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| 10K+ SKUs with rich content across 3+ channels | <500 SKUs with basic attributes | Direct ERP-to-eCommerce |
| Product descriptions need translations and channel-specific formatting | Single-language, same content across all channels | eCommerce platform built-in product management |
| Multiple teams collaborate on product content | Single person manages product data | Spreadsheet or ERP-native fields |
| Selling on multiple channels with different attribute requirements | Single ecommerce channel | Channel-native product management |
| Compliance requires audit trails for product data changes | No regulatory requirements for product data governance | Simpler integration pattern |
| Capability | Akeneo PIM | Salsify PXM | inRiver iPMC | Stibo STEP |
|---|---|---|---|---|
| Deployment | SaaS or on-premise | SaaS only | SaaS only | SaaS, on-prem, hybrid |
| API Style | REST + GraphQL | REST + GraphQL | REST | REST + STEPXML + SOAP |
| Bulk Import | 100/batch via API | 50K/job; unlimited SFTP | 500/batch | STEPXML unlimited |
| Event-Driven | Event Platform (Enterprise+) | Webhooks | Webhooks | Events + workflows |
| SAP Connector | Accelerators on SAP BTP | Partner connectors | Built-in | Built-in |
| Open Source | Community Edition | No | No | No |
| DAM | Basic built-in + connectors | Full built-in | Full built-in | Connectors only |
| Syndication | Via connectors | Built-in (strongest) | Built-in feeds | Via integrations |
| MDM Overlap | PIM only | PIM + content | PIM only | Full MDM + PIM |
| Best For | Developer-friendly; open | Brands; retail/marketplace | B2B manufacturers | Enterprise MDM + PIM |
| Typical Price | Free (CE); $30K-150K/yr | $100K-500K/yr | $80K-300K/yr | $150K-500K/yr |