Skip to content
APIs, middleware & architecture

Bulk Import from the ERP: Using the Shopware Sync API

Bulk import from the ERP into Shopware: the Sync API with upsert and delete, package sizes set by server limits, indexing via the message queue and delta sync.

19 min read Sync-APIMassenimportShopwareWarenwirtschaft

In short and in plain language

Before big sales, the stock system changes many prices. This data has to get into the shop fast.

If you send each item on its own, it takes a long time. The shop gets slow.

Shopware has a way to send many items in one go. It is called the Sync API. It takes many items in one package.

Then the shop has to sort its search and lists again. A queue can do this in the background. Small programs work through the tasks there.

It is good to send only the data that has changed. We help you set up the import this way.

Ahead of the November sales season, a lot changes in the inventory system at once: new prices, strike-through prices, stock for promotional items, freshly created bundles. To get this data into the shop on time, the ERP writes thousands of records into Shopware. If that runs as one call per item, with indexing inside the running request, the import slows down itself and the shop along with it. Shopware offers a dedicated route for this case: the Sync API, which bundles several write operations into one call (Shopware Admin API reference). This article shows how a sync package is structured, how to size packages against the limits of web server and PHP, why indexing belongs in the message queue, how many workers make sense, how to evaluate errors per operation, and why a delta sync replaces the nightly full sync in day-to-day operations.

Key takeaways

  • The Sync API accepts upsert and delete operations for several entities in one call at /api/_action/sync. Since version 6.5 it writes all records of a call within one transaction (Shopware UPGRADE-6.5).
  • Package size follows the limits of the environment: without changes, nginx accepts a request body of at most 1 MB (nginx documentation), and by default PHP terminates a script after 30 seconds (PHP manual).
  • The header indexing-behavior: use-queue-indexing moves indexing out of the request into the message queue. Product indexing is usually slow there and can be routed to its own transport with its own workers where needed (Shopware documentation).
  • Shopware calls up to three worker processes normal for the database queue; beyond that, a dedicated message broker is needed. Failed messages are retried 3 times and then deleted (Shopware documentation).
  • A delta sync transfers only changed prices, stock levels and deletions. Error paths built from operation key and position trace every rejected record back to the ERP record that needs fixing.

Why single calls slow down a bulk import

Many integrations start with the obvious approach: for every changed item, the middleware sends its own call to the Shopware Admin API. With a few dozen changes a day, this hardly matters. Before a promotion, things look different. Prices and stock levels then change for large parts of the catalog at the same time, and every one of these calls goes through the same steps: open a connection, check the token, validate data, write, dispatch events and recalculate dependent data.

The last step is the expensive one. Depending on which data was written, Shopware triggers various indexing processes; with large imports this puts a high load on the server (Shopware Admin API reference). If this work runs in the same request as the write, the import waits for indexing, and PHP processes that are supposed to serve visitors are busy recalculating. The shop gets slower, precisely in the days when customers compare prices.

Then there is the ERP side. Cloud ERP systems limit the number of requests per time window; how to control an export there is covered in our article on API limits in bulk imports. On the shop side, a different tool helps: fewer but larger calls whose indexing runs decoupled in the background. Typical signs that an import starts at the wrong end:

  • The import run takes hours and overlaps with the times when customers place orders.
  • Storefront response times rise for as long as the import is running.
  • After the import, listings and search still show old prices or stock levels for some time.
  • An abort in the middle of a run leaves some items with new values and others with old ones.
  • The log does not show which ERP record was rejected and why.

What the sync endpoint can do

The Sync API is an add-on to the Admin API that lets you perform several write operations, meaning create, update and delete, in a single call. All entities that can be written via the Admin API can also be written via the Sync API (Shopware Admin API reference). The endpoint is located at /api/_action/sync and expects a POST request with JSON in the body.

A call contains a list of operations. Each operation names the action (upsert or delete), the entity and the payload: a list of records for an upsert, a list of IDs for a delete. Within one request, different entities can be written together, and for easier debugging every operation gets its own key (Shopware Admin API reference). For an import from the inventory system, this means that prices, stock levels and category assignments of one run travel in one package. The following example updates the stock of two items, assigns an item to a category via its product number and removes another item from a category.

sync-package.http
POST /api/_action/sync
Content-Type: application/json
indexing-behavior: use-queue-indexing
sw-skip-trigger-flow: true

{
  "stock-run-0412": {
    "entity": "product",
    "action": "upsert",
    "payload": [
      { "id": "0190c8a2e5f47b1d9a3c6e8f2b4d6a10", "stock": 140 },
      { "id": "0190c8a2e5f47b1d9a3c6e8f2b4d6a11", "stock": 0 }
    ]
  },
  "category-assign-run-0412": {
    "entity": "product_category",
    "action": "upsert",
    "payload": [
      {
        "productId": { "resolver": "product.number", "value": "A-10023" },
        "categoryId": "0190c8a2e5f47b1d9a3c6e8f2b4d6a55"
      }
    ]
  },
  "category-removal-run-0412": {
    "entity": "product_category",
    "action": "delete",
    "payload": [
      {
        "productId": "0190c8a2e5f47b1d9a3c6e8f2b4d6a10",
        "categoryId": "0190c8a2e5f47b1d9a3c6e8f2b4d6a77"
      }
    ]
  }
}

Under data, the response lists the written IDs per entity, along with notFound and deleted (Shopware Admin API reference). Choose a meaningful operation key, for example from data type and run number: it appears in error messages as part of the path and thus links every message to the run the record came from. The two headers in the example move indexing into the queue and suppress flows; this article covers both further below.

Upsert, delete and foreign keys in the package

Unlike the rest of the Admin API, the Sync API does not distinguish between create and update. For each record it checks whether the entity already exists and updates it if an ID was passed; otherwise a new entity is created (Shopware Admin API reference). For an import this is convenient and at the same time the most common source of errors: anyone who sends no ID, or generates a new one per run, creates duplicates instead of updating. The ID of an item therefore has to be derived from a stable key and stored in the data mapping between ERP and shop. For an ERP import, these building blocks of the Sync API matter most:

Upsert

Creates or updates, depending on whether the ID exists. The ID comes from the mapping, not from the random generator of the import run.

Delete

Deletes records by their IDs. For mapping tables such as product_category, these are the two foreign keys that together form the primary key.

Delete by criteria

Removes assignments via a filter without listing every combination. Criteria and payload cannot be combined in the same operation.

Foreign key resolvers

Reference other entities by readable keys such as product number or ISO code. The API resolves them to UUIDs before writing.

Operation key

Names each operation in the package. Error messages carry it in the path, so the log can find the run and the data type.

Suppress flows

The header sw-skip-trigger-flow prevents an import from triggering flows, such as emails to customers after a data change.

Foreign key resolvers are particularly useful for ERP imports, because the ERP usually does not know the Shopware UUIDs, but it does know product numbers and ISO codes. The reference uses product.number as its example; version 6.7.11.0 added seven more resolvers, including currency.iso_code, tax.tax_rate and payment_method.technical_name (Shopware release notes 6.7.11.0). tax.tax_rate only resolves when exactly one tax record has that rate. If a value cannot be resolved, the request fails; with nullOnMissing, the reference falls back to null instead (Shopware Admin API reference). In the source code, Shopware applies the resolvers only to upsert operations, so deletes still need the UUID (Shopware source code). Before you rely on them, check which resolvers your installed version knows.

Do not delete relations via the main entity

An assignment, such as an item in a category or a property on an item, does not disappear when it is missing from the upsert of the item. It is deleted as a delete on the mapping entity, for example product_property; the reference also describes deleting via criteria for this (Shopware Admin API reference). Otherwise an import that only writes items collects orphaned assignments over the months.

Finding the right package size

The Sync API reference does not state a fixed maximum number of records per call. The limit is set by the environment, at several points at once. First, the web server limits the size of the request body: without changes, nginx accepts at most 1 MB and answers larger bodies with status 413 (nginx documentation). Under the HTTP standard, 413 means that the server refuses the request because its content is larger than it is willing or able to process (RFC 9110).

PHP comes next. By default, a script may allocate 128 MB of memory (PHP manual); Shopware requires at least 512 MB and a maximum execution time of at least 30 seconds (Shopware documentation). The whole package is decoded, validated and written in one transaction on receipt, so a large package uses more memory and holds database locks longer. For the database, Shopware recommends a max_allowed_packet of at least 32 MB (Shopware documentation). The most important limits at a glance:

LimitDefaultWhere it appliesConsequence when exceeded
client_max_body_size (nginx)1 MB (nginx documentation)in the web server, before PHP sees the packagestatus 413, the package does not reach Shopware
memory_limit (PHP)128 MB (PHP manual), Shopware requires at least 512 MB (Shopware documentation)while decoding, validating and writing the packageabort with a memory error, outcome of the run unclear
max_execution_time (PHP)30 seconds (PHP manual)in the PHP script of the web request, including synchronous indexingscript terminated, the client receives no usable response
max_allowed_packet (database)Shopware recommends at least 32 MB (Shopware documentation)for large statements between PHP and databasewrite error, the package is not applied

So there is no universal value, but a measuring procedure. Records differ in weight: a stock sync carries one ID and one number per item, a complete item with translations, properties and price rules a multiple of that. Separate package sizes per data type therefore make sense, determined in a test environment with real data volumes. Because since version 6.5 all records of a call are validated together and written in one transaction (Shopware UPGRADE-6.5), package size has a second effect: one faulty record holds up the whole package. Smaller packages make every retry cheaper. Here is how to proceed:

  1. For each data type, export a typical and a particularly large record from the ERP and measure their size as JSON.
  2. Read the actual limits of the target environment: body limit of the web server, memory_limit and max_execution_time of the PHP-FPM configuration.
  3. Start with a small package and increase the size step by step as long as response time and memory peak stay well below the limits.
  4. Fix the value with a margin to the limit, so that records with long descriptions or many translations do not push the package over it.
  5. Log package size, run time and rejected records per run, and measure again after updates to Shopware or the server.

Indexing via the message queue

After writing, Shopware is not done yet. Depending on the written data, indexing processes run, for example for search keywords, stock or SEO URLs. Without further instruction, Shopware indexes synchronously, within the same request. For large imports, indexing can be controlled via headers and moved to an asynchronous background process (Shopware Admin API reference). Our article on event-driven architecture with message queues explains the basics of queues, producers and consumers.

Header and valueEffectUse in the import
indexing-behavior: null (default)synchronous indexing in the requestsmall corrections that should be visible immediately
indexing-behavior: use-queue-indexingasynchronous indexing via the message queuestandard case for bulk imports, workers process the messages
indexing-behavior: disable-indexingindexing completely disabledonly with a planned reindex afterwards, for example for an initial load
indexing-skipskips the named indexers, comma-separatedwhen a run demonstrably does not touch certain data
indexing-onlyruns only the named indexerspure stock sync, for example only product.stock

For bulk imports, use-queue-indexing is the standard case. The request returns as soon as the data is written, and the recalculation is distributed across the workers. disable-indexing is a tool for special cases: anyone who uses it has to trigger indexing afterwards, for a complete run with dal:refresh:index --use-queue. Shopware recommends the --use-queue option because otherwise too many products would have to be indexed without a queue (Shopware documentation).

One sentence that the message queue documentation places before worker planning is key: product indexing messages are usually slow, while other messages are processed very fast (Shopware documentation). If both share the same queue, time-critical messages, such as emails if they run through the queue, wait behind thousands of indexing jobs. The documentation therefore advises routing messages to a different transport where needed, to limit the number of workers per message type, avoid database locks or prioritize sending emails, for example.

Put indexing on its own transport

Since Shopware 6.6.4.0 and 6.5.12.0, the routing of individual message classes can be overwritten with routing_overwrite (Shopware documentation). The documentation shows this with the EntityIndexingMessage, which goes to its own transport entity_indexing. This transport gets its own workers, whose number you set independently of the rest of the operation.
config/packages/*.yaml
# config/packages/framework.yaml
framework:
  messenger:
    transports:
      entity_indexing: "%env(MESSENGER_TRANSPORT_INDEXING_DSN)%"

# config/packages/shopware.yaml
shopware:
  messenger:
    routing_overwrite:
      'Shopware\Core\Framework\DataAbstractionLayer\Indexing\EntityIndexingMessage': entity_indexing

How many workers the queue needs

Shopware considers a general recommendation difficult: the number of workers depends on the volume and type of messages; you should monitor the queue and adjust the workers accordingly (Shopware documentation). The search setup guide offers a reference point: in Shopware's experience, up to three worker processes are normal and useful in production; anyone who needs more uses a dedicated message broker so that the database does not become a bottleneck (Shopware documentation).

Where the workers run matters as much as their number. Without further setup, Shopware processes background tasks in the browser as long as someone is logged into the Administration; this is not recommended for production systems (Shopware documentation). A nightly import with use-queue-indexing would then wait for its indexing until someone opens the Administration in the morning. In production, workers belong under a process manager such as systemd, running as console processes. The example in the documentation starts each worker with a time limit of 60 seconds and a memory limit of 512 MB; the process manager then restarts it (Shopware documentation).

Two points are easily overlooked during setup. First, the queue for failed messages also needs a worker, otherwise those messages stay where they are; Shopware retries a failed message automatically 3 times and deletes it afterwards (Shopware documentation). Second, workers only pick up new code after a restart. The Symfony documentation recommends not letting workers run indefinitely, for example ending them after ten messages with --limit=10, and restarting them on every deploy (Symfony documentation). And because the console and PHP-FPM often read different php.ini files (Shopware documentation), different limits apply to workers than to the web request: on the command line, the default execution time is 0, meaning no limit (PHP manual). The queue fill level therefore belongs in the monitoring of your interfaces. An import run then looks like this:

Flow of an import run via the Sync API

  1. 1

    Determine the delta

    The ERP delivers all prices, stock levels and deletions changed since the last successful run.

  2. 2

    Build packages

    The middleware splits the changes by data type into packages of the measured size and assigns meaningful operation keys.

  3. 3

    Send

    Each package goes to /api/_action/sync with indexing-behavior: use-queue-indexing and sw-skip-trigger-flow.

  4. 4

    Evaluate the response

    Written IDs, notFound and error paths are logged per package, rejected records are filed separately.

  5. 5

    Process the queue

    The workers index in the background; the queue fill level shows when the changes are visible in the shop.

  6. 6

    Check a sample

    Some changed items are checked in the storefront against the ERP before the run counts as done.

Evaluating errors per operation

Older versions could be called so that individual records were written separately. Version 6.5 removed this behavior: all operations and records of a call are validated as a collection and written within one transaction, and the headers fail-on-error and single-operation no longer exist (Shopware UPGRADE-6.5). For the import this means: either all records of the package are written or none.

Which records held up the package is stated in the error response. Shopware collects the validation errors of all records before it aborts and sets a path for each record built from operation key and position in the package (Shopware source code). An error under /stock-run-0412/17/ therefore points to the record with index 17 in the operation stock-run-0412; counting starts at zero. The middleware must be able to trace this path back to the ERP record, otherwise only a manual search remains. These rules have proven themselves:

  • Write rejected records to a queue for the business department and resend the rest of the package without them.
  • On status 413, split the package and lower the package size for this data type instead of aborting the run.
  • On a timeout or dropped connection, repeat the package unchanged; thanks to upsert with stable IDs, no duplicates arise.
  • Log notFound from delete operations: a record missing in the shop often points to a gap in the mapping.
  • Record the number of packages, written IDs, rejected records and duration per run, so that outliers stand out.

How to limit retries cleanly is covered in our articles on idempotency and retry strategies and on error handling in interfaces. For the Sync API, one more rule applies: a package that fails on a single record must not be retried in a loop before that record has been taken out. Which error responses your middleware expects and how it reacts to them belongs in the description of the interface.

Delta instead of full sync

The biggest lever comes before the first package, in the question of what gets transferred at all. A full sync sends the entire catalog every night, including the items that have not changed. Each of these records is written and triggers indexing. A delta sync transfers only what has changed since the last successful run. This requires a reliable change marker per record in the ERP, such as a timestamp or a change log, and the middleware keeps track of the state of the last successfully completed run.

Two points decide whether a delta sync is reliable. Deletions have to come from the ERP as separate events, because a deleted item no longer appears in any change list. And data ownership has to be settled per field: if marketing maintains descriptions in the shop, the import must not overwrite that field. How to resolve such conflicts is covered in the article on bidirectional synchronization. Prices and stock levels sensibly run as separate, small deltas apart from the master data, as the articles on B2B price synchronization and inventory sync across several warehouses also describe.

An occasional full sync still makes sense, but as a control: it compares checksums or samples between ERP and shop and writes only the deviations. It is scheduled in a quiet time window, not in the week before a promotion. The difference at a glance:

AspectFull syncDelta sync
Records transferredentire catalog per runonly changes since the last run
Indexing loadfor every transferred itemonly for changed items
Deleted itemsnoticed by their absenceneed their own deletion events from the ERP
Run timegrows with the cataloggrows with the number of changes
Role in operationscontrol in quiet time windowsregular operation, also during the day

Schedule the import before the promotion window

For the sales season, this means: the large import belongs before the promotion window, not inside it. Bundles, master data and stock for promotional items should be in the shop a few days ahead, so that indexing, caches and checks are complete before the rush begins. Prices that only apply from a certain date can be created in advance and tied to a rule with a time range, instead of importing them the night before the promotion. During the promotion days, only small stock deltas run. How to move nightly bulk jobs out of the promotion window and secure the order flow toward the ERP is covered in the article on the order peak during Black Week.

The other background jobs belong in the plan as well. As a scheduled task, sitemap generation can block the queue for an extended period; since version 6.7.1.0 it can be taken out of the queue and started via cron job at quiet times (Shopware documentation). With a large catalog or a high update frequency, the indexer for dynamic product groups can become a bottleneck; from version 6.6.10.0 it can be disabled, though with side effects on category pages and rules (Shopware documentation). Such changes should be tested before the promotion, not in its first night. A sensible sequence for the weeks before:

  1. Measure package sizes and the number of workers in the test environment with the data volumes of the promotion.
  2. Put indexing on its own transport and add the queue fill level to monitoring.
  3. Import bundles, master data and promotional prices created in advance, and spot-check them in the storefront.
  4. Suspend full syncs and other large jobs for the promotion days.
  5. During the promotion, run only stock and price deltas and watch the queue fill level.

Whether your existing integration already works this way shows in three things: how many calls a run produces, where indexing takes place and whether rejected records are filed in a traceable way. We review imports as part of our API development and build them, where several systems need to be connected, as part of a middleware. For an initial assessment, reach us via the contact page.

One package, one queue, one delta

A fast import depends less on bigger hardware than on three decisions: send data bundled via the Sync API, process indexing via the message queue with dedicated workers, and transfer only what has changed. Setting these three levers based on measurements keeps shop and import apart, even before the sales season.
This article is based on data from: Shopware Admin API reference, Bulk Payloads page; Shopware documentation, pages Message Queue, Hosting, search setup and Performance tweaks; Shopware release notes 6.7.11.0; Shopware UPGRADE-6.5; Shopware source code (SyncService, EntityWriter, ApiRequestContextResolver); nginx documentation, module ngx_http_core_module; PHP manual, configuration options; RFC 9110 of the IETF; Symfony documentation, Messenger.

Related Articles

APIs, middleware & architecture

Documenting Interfaces: OpenAPI Instead of Tribal Knowledge

Describe shop-ERP interfaces with OpenAPI: fields, error codes, examples and scopes, the description as a contract and the handover package for a switch.

18 min read
Shop integration & processes

DATEV Rechnungsdatenservice 2.0 ends: switch the data route

DATEV discontinues its Rechnungsdatenservice 2.0 on 31 Dec 2026. How shops and ERP connectors check whether they are affected and move their document route.

19 min read
ERP & merchandise management

Black Week order peak: preparing your ERP interface

Black Week 2026: prepare the order flow between shop and ERP – measure the backlog, move night jobs, add a queue with alerts, freeze changes, define a fallback.

18 min read