ERP-to-PIM Integration: Product Information Management Playbook

Type: ERP Integration Systems: Akeneo PIM, Salsify PXM, inRiver iPMC, Stibo STEP + ERP sources Confidence: 0.85 Sources: 8 Verified: 2026-03-03 Freshness: 2026-03-03

TL;DR

System Profile

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]

SystemRoleAPI SurfaceDirection
ERP (SAP, NetSuite, D365)Operational product master — SKU, cost, weight, dims, complianceOData v4, REST, SOAP, SuiteTalkOutbound (ERP -> PIM)
PIM (Akeneo, Salsify, inRiver, Stibo)Content enrichment hub — descriptions, media, translationsREST, GraphQL, WebhooksInbound + Outbound
DAM (Cloudinary, Bynder, Widen)Media asset management — images, videos, 3D modelsRESTBidirectional with PIM
iPaaS / MiddlewareIntegration orchestration — mapping, transformation, error handlingN/AOrchestrator
eCommerce / MarketplaceSales channels — Shopify, Magento, Amazon, eBayREST, GraphQLInbound (PIM -> channel)

API Surfaces & Capabilities

PIM SystemAPI SurfaceProtocolBest ForBulk ImportWebhook SupportEvent-Driven?
Akeneo PIMREST API v1HTTPS/JSONProduct CRUD, attribute management100/batch PATCHEvent Platform (Enterprise+)Yes
Akeneo PIMGraphQL APIHTTPS/GraphQLComplex product queriesNoN/ANo
Salsify PXMREST APIHTTPS/JSONProduct CRUD, digital assets50K/jobYesYes
Salsify PXMGraphQL APIHTTPS/GraphQLComplex queries, nested relationshipsNoN/ANo
Salsify PXMSFTPSFTP/CSVLegacy bulk import, scheduled exportsUnlimited (chunked at 50K)N/ANo
inRiver iPMCREST APIHTTPS/JSONEntity CRUD, link management500/batchYesYes
Stibo STEPREST APIHTTPS/JSONProduct CRUD, workflowsSTEPXML (no limit)YesYes
Stibo STEPSTEPXMLXML/HTTPSBulk import/export, migrationUnlimitedN/ANo

Rate Limits & Quotas

Per-Request Limits

PIM SystemLimit TypeValueNotes
AkeneoProducts per PATCH batch100Use /api/rest/v1/products with JSON array
AkeneoRequest body size50 MBApplies to product import payloads
AkeneoMedia file size100 MB per assetLarger assets require chunked upload
SalsifyProducts per bulk import job50,000Auto-chunks internally
SalsifyAPI response page size250 recordsCursor-based pagination
inRiverEntities per batch write500Batch entity creation endpoint
Stibo STEPSTEPXML import file sizeNo hard limitPractical limit ~500MB

Rolling / Daily Limits

PIM SystemLimit TypeValueWindowEdition Differences
Akeneo (SaaS)API callsManaged throttlingRollingSerenity: higher burst; Growth: lower concurrency
Akeneo (on-prem)API callsConfigurableN/ANo vendor-imposed limit
SalsifyAPI callsFair use ~100-300 req/minPer minute429 + Retry-After header
inRiverAPI callsFair use — throttled per tenantPer minuteEnterprise: higher concurrency
Stibo STEPAPI callsConfigurableN/AOn-premise: unlimited; SaaS: managed

Authentication

PIM SystemAuth MethodUse WhenToken LifetimeRefresh?Notes
AkeneoOAuth 2.0 Client CredentialsServer-to-server integration1 hourYesGenerate in PIM > Connections
SalsifyAPI Key + Org IDAll integrationsNo expiryN/ABearer token; scoped per user
inRiverAPI KeyAll integrationsNo expiryN/AX-inRiver-APIKey header
Stibo STEPOAuth 2.0 / SAMLCloud or hybridSession-basedYesOn-prem supports basic auth

Authentication Gotchas

Constraints

Integration Pattern Decision Tree

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

Quick Reference

End-to-End Integration Flow

StepSourceActionTargetData ObjectsFailure Handling
1ERPExport product master data (SKU, cost, weight, dims)iPaaSItem master recordsRetry 3x, then DLQ
2iPaaSTransform: map ERP fields to PIM attribute familiesPIMProduct entitiesValidation errors -> error log
3PIMEnrichment: descriptions, media, translations, SEOPIM (internal)Enriched productsCompleteness workflow notifications
4PIMCompleteness gate passed -> ready for channelPIM (internal)Channel-ready productsIncomplete -> held in workflow
5PIMSyndication: export channel-specific product feedeCommerce / MarketplaceChannel-formatted catalogPer-channel error logs; retry
6ERPPrice/inventory update (bypasses PIM)eCommercePricing + stockDirect ERP-to-channel
7eCommerceOrder placed (triggers O2C flow)ERPSales orderSeparate O2C integration

PIM System Comparison

CapabilityAkeneo PIMSalsify PXMinRiver iPMCStibo STEP
API StyleREST + GraphQLREST + GraphQLRESTREST + STEPXML + SOAP
Bulk Import100/batch via API50K/job; unlimited SFTP500/batchSTEPXML (no limit)
Event-DrivenEvent Platform (Enterprise+)Webhooks (all editions)Webhooks (all editions)Outbound events + workflows
AuthenticationOAuth 2.0API Key + Org IDAPI KeyOAuth 2.0 / SAML
DAM IntegrationBuilt-in basic + connectorsFull built-in DAMFull built-in DAMConnectors only
Channel SyndicationVia connectorsBuilt-in engine (strongest)Built-in feedsVia integrations
SAP ConnectorAccelerators on SAP BTPPartner connectorsStandard connectorDedicated connector
Open SourceCommunity Edition (free)NoNoNo
Best ForDeveloper-friendly; open ecosystemBrand manufacturers; retail/marketplaceB2B manufacturers; complex productsEnterprises needing MDM + PIM

Step-by-Step Integration Guide

1. Define data ownership matrix

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.

2. Map ERP product types to PIM attribute families

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.

3. Configure ERP product export

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.

4. Transform and load into PIM

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.

5. Configure channel syndication

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.

6. Implement delta sync and reconciliation

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.

Code Examples

Python: Full ERP-to-Akeneo PIM Sync Pipeline

# 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

JavaScript/Node.js: inRiver Entity Import

// 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;
}

cURL: Quick Akeneo API Test

# 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":[...]}}

Data Mapping

Field Mapping Reference

ERP Field (SAP S/4HANA)PIM Field (Akeneo)TypeTransformGotcha
MATNR (Material Number)identifier (SKU)StringStrip leading zerosSAP pads to 18 chars; PIM excludes padding
MAKTX (Material Description)name (localizable)StringMap per localeERP description is internal, not customer-facing
BRGEW (Gross Weight)weight (metric)Decimal + UnitConvert SAP KG -> Akeneo KILOGRAMSAP stores 3 decimal places; PIM may differ
MEINS (Base UoM)base_unitEnumMap UoM codesSAP has 500+ codes; PIM supports 20-50
MATKL (Material Group)family + categoriesMapping tableLookup table requiredHardest mapping — requires business input
EAN11 (GTIN/EAN)ean (global)StringValidate check digitFilter placeholder GTINs (all zeros)
PRDHA (Product Hierarchy)categories (multi-value)String -> ArraySplit hierarchy codesSAP uses 18-char concatenated; PIM uses tree
FERTH (Production/Batch)N/A — skipN/ADo not syncInternal manufacturing data

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

CodePIM SystemMeaningCauseResolution
401AllUnauthorizedExpired token or revoked keyRefresh OAuth; verify API key active
422AkeneoUnprocessable entityInvalid attribute, missing required fieldCheck error message; validate against family
429AllRate limit exceededToo many API callsExponential backoff; respect Retry-After
404AllEntity not foundReferencing non-existent product/familyCreate prerequisites first
400SalsifyBad requestMalformed JSON, invalid propertyValidate payload against schema
409inRiverConflictConcurrent update to same entityRetry with jitter; optimistic locking

Failure Points in Production

Anti-Patterns

Wrong: Syncing live pricing through PIM

# BAD — routing price updates through PIM adds 15-60 min latency
ERP (price change) -> iPaaS -> PIM -> iPaaS -> eCommerce
# Customers see stale pricing

Correct: Direct ERP-to-channel for hot data

# 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

Wrong: Full catalog rescan on every sync

# 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

Correct: Delta sync with change tracking

# GOOD — only sync changed products
changed = erp.get_products(modified_after=last_sync)  # ~50 records
pim.batch_import(changed)  # 1 API call

Wrong: Embedding base64 media in product API calls

# BAD — 50MB payload, timeout, 413 Payload Too Large
product["hero_image"] = base64_encode(read_file("product.jpg"))

Correct: Upload media separately, then reference by code

# 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]

Common Pitfalls

Diagnostic Commands

# 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

Version History & Compatibility

SystemVersionReleaseStatusBreaking ChangesNotes
Akeneo PIMv7.02024-03CurrentEvent Platform GA; new permissionsGraphQL expanded
Akeneo PIMv6.02023-03SupportedAsset Manager replaced PAMMigrate from PAM
Akeneo PIMv5.02022-01EOLNew category tree APIMinimum for Event Platform
Salsify PXM20252025-01CurrentGraphQL API GAEnhanced bulk ops
inRiver iPMC20252025-01CurrentNoneEnhanced REST endpoints
Stibo STEP20252025-01CurrentREST API v2Legacy SOAP deprecated

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
10K+ SKUs with rich content across 3+ channels<500 SKUs with basic attributesDirect ERP-to-eCommerce
Product descriptions need translations and channel-specific formattingSingle-language, same content across all channelseCommerce platform built-in product management
Multiple teams collaborate on product contentSingle person manages product dataSpreadsheet or ERP-native fields
Selling on multiple channels with different attribute requirementsSingle ecommerce channelChannel-native product management
Compliance requires audit trails for product data changesNo regulatory requirements for product data governanceSimpler integration pattern

Cross-System Comparison

CapabilityAkeneo PIMSalsify PXMinRiver iPMCStibo STEP
DeploymentSaaS or on-premiseSaaS onlySaaS onlySaaS, on-prem, hybrid
API StyleREST + GraphQLREST + GraphQLRESTREST + STEPXML + SOAP
Bulk Import100/batch via API50K/job; unlimited SFTP500/batchSTEPXML unlimited
Event-DrivenEvent Platform (Enterprise+)WebhooksWebhooksEvents + workflows
SAP ConnectorAccelerators on SAP BTPPartner connectorsBuilt-inBuilt-in
Open SourceCommunity EditionNoNoNo
DAMBasic built-in + connectorsFull built-inFull built-inConnectors only
SyndicationVia connectorsBuilt-in (strongest)Built-in feedsVia integrations
MDM OverlapPIM onlyPIM + contentPIM onlyFull MDM + PIM
Best ForDeveloper-friendly; openBrands; retail/marketplaceB2B manufacturersEnterprise MDM + PIM
Typical PriceFree (CE); $30K-150K/yr$100K-500K/yr$80K-300K/yr$150K-500K/yr

Important Caveats

Related Units