For the complete documentation index, see llms.txt. This page is also available as Markdown.

Onlineshops and Marketplaces

Connect a shop or marketplace: sales channels, listings, outbound sync and order import.

A shop app connects an online shop or marketplace — Shopware 6, Shopify, Amazon, eBay, Channelpilot — to the ERP. Products, prices, stock and images go out; orders come in.

Read App Backends first; this page only covers what is specific to this domain.

Backend vs. channel

Sales channel backend

Sales channel

How many

One per app per tenant

Many — one per connected store

Represents

The integration itself ("Shopware 6")

One shop or marketplace account

Created

On install

On install (a default one) and by the user afterwards

validChannelTypes on the backend restricts which channel types users may attach. Use ['ECOMMERCE'] for shops and ['MARKETPLACE'] for marketplaces (POS and STORE also exist).

await migratorErp.setMigration('salesChannelBackend', async (methods) =>
{
  let backend = await methods.findSalesChannelBackend();

  if (backend)
  {
    backend = await methods.getSalesChannelBackend(backend.id);
    await methods.activateSalesChannelBackend(backend);
  }
  else
  {
    backend = await methods.createSalesChannelBackend('My Shop', ['ECOMMERCE']);
  }

  const channels = await methods.getSalesChannels();

  if (channels?.length) { return null; }   // never create a second default channel

  const channel = await methods.createSalesChannel(backend, 'My Shop', '', 'ECOMMERCE');

  return channel.id;                        // stored as the migration note
});

Three details in that snippet matter:

  • Reactivate on reinstall. A deactivated backend disappears from the ERP's channel-creation dialog, so users can no longer create channels for your app.

  • findSalesChannelBackend() returns only an id. Re-read the full record before patching it.

  • Guard against a second default channel, or every reinstall adds one.

Only the first sales channel is license-free. Creating another without a license produces a 402, which the framework swallows — logging a warning and returning an empty object. Your migration continues, so code that expects channel.id must tolerate its absence.

Serving many channels from one app

Everything channel-specific is keyed by the channel id:

Concern
How it is scoped

Requests

The X-Sales-Channel header, pinned into the request context

Configuration

Per-channel parameters stored in your app

Credentials

Per channel — one tenant may connect several stores

Webhooks

Channel-scoped events, and the channel id in the callback URL

Scheduled work

One cron task per channel

Register handlers for sales_channel.create and sales_channel.delete, and provision everything above in the create handler. That is what makes a second store work without a reinstall.

Migration order

The order of steps inside your migration is the order they run, and two orderings bite:

1

Register webhooks after the channel exists

Channel-scoped callback URLs carry the channel id, which you read from the previous step's result. Register first and you get subscriptions pointing at ?salesChannel=null — permanently, because the wrong URL is part of a subscription's identity. Every existing shop app carries a cleanup migration for exactly this.

2

Create import presets before the channel is created

The sales_channel.create handler looks them up.

Also: create an EAV group in one step and write to it in a separate step, so a retry of the write doesn't try to recreate the group.

Listings

Each article can have one listing per sales channel, holding the channel-specific data — above all the external platform id.

Query listings through the article.shopListing view, filtered by your channel:

Listing state is a handshake between the ERP and your app:

Set by the ERP
Set by your app

DISABLED, NEED_SYNCHRONIZE, NEED_DELETION

QUEUED_FOR_SYNCHRONIZE, QUEUED_FOR_DELETION, SYNCHRONIZED, ERRONEOUS

Move a listing to QUEUED_FOR_SYNCHRONIZE when you enqueue it, and to SYNCHRONIZED or ERRONEOUS when the sync finishes. Await that final write — if it fails silently, the queue entry is marked complete while the listing stays stuck and gets re-enqueued forever.

Skip listings that are DISABLED. Otherwise a channel-creation backfill enqueues the entire catalogue.

Storing external ids

Use custom fields. Two groups are conventional:

  • a group named after your app with a single id attribute, attached to the entities you need;

  • a group named <app>_article_listing on article_listing for richer per-listing data.

Attach the group only to the entities that actually need it. A group shared across accounts, documents, listings, media and variants means an id written for one entity type leaks onto all the others — a real problem that had to be undone later with a migration.

Also note that removing data from a group removes it across every entity the group is attached to.

When you write an external id back to a listing, set x-vario-suppress-own-webhooks — otherwise your write re-triggers the article event you subscribe to, and you loop. See the FAQ.

Outbound: the queue

Webhooks fire far more often than a platform API should be called, so outbound work goes through a queue in your app:

Entries move QUEUED → PROCESSING → COMPLETED, or to FAILED. Points worth knowing:

  • Check the transfer toggle when inserting, not when processing. Then the queue only ever holds work the user has enabled.

  • Deduplicate against both QUEUED and PROCESSING. Checking only QUEUED lets a webhook that arrives while the worker is busy create a duplicate.

  • Dedup is check-then-act, so it races. Add an in-process guard per entity — and be aware it does not span processes, so a multi-instance deployment still needs a shared guard.

  • A FAILED entry is not retried automatically. The drain only picks up QUEUED. Offer a retry action.

  • Mask errors before persisting them. The stored request extract is shown in the UI and can contain credentials or customer data.

If the platform applies a whole batch in one transaction, a single bad item rolls back the rest — put the successful entries back to QUEUED rather than marking them complete.

Inbound: order import

Orders come in through the ERP's multi-part import:

The flow is: copy your import preset, stream the data up as a DMS file, create the import runs, wait until the data is extracted, then validate or execute. Presets are created during your migration and define the field mapping per file.

Deduplicate inside the import script by looking for an existing document carrying the platform's order id. That single guard is what makes the whole pipeline safe to re-run — and it is why you should keep other identifiers in their own attributes rather than reusing the order-id field.

Because of that guard, an overlap is cheap and a gap is not:

  • Advance your "imported up to" watermark only after a successful import — never in validation-only mode.

  • Query a little further back than the watermark. Platform feeds are eventually consistent, and an order can surface after your cursor has moved past it. Re-seen orders are dropped by the dedup guard.

What else to watch out for

Tax mapping is by name, and names are fragile. Mapping an ERP tax type to a platform tax class by name breaks as soon as the platform name contains a space or a % — the value cannot be safely interpolated and is dropped, silently losing the price. Validate names, and prefer ids where the platform offers them.

Platform limits are low. Shopware caps every search at 500 results; Amazon's SP-API is aggressively rate limited and reports its own limits in response headers. Chunk id lists and paginate.

Some pagination is state-driven. If your fetch filters on a status you then change, the result set shrinks by itself and you must not pass a page number — but in validation-only mode nothing changes, so you must not paginate at all or you loop forever on page one.

A stored external reference is not proof the remote object exists. With transactional platform APIs, a rolled-back sync can leave you holding a reference to something that was never created. Verify before pointing new objects at it.

Uninstall leaves almost everything in place — webhooks, cron tasks and the backend all survive. Reinstall therefore has to be idempotent and must reactivate the backend.

Last updated

Was this helpful?