Shopify Multichannel: Syncing 7+ Sales Channels Without Breaking the Catalog
Replacing an 18MB Excel macro with a real inventory pipeline — Amazon Seller and Vendor Central, Google Merchant Center, dropship networks, and why every channel has a different key column.
The real multichannel problem
Everyone thinks the multichannel problem is listing products on more channels. It is not. Listing is a one-time cost.
The problem is that once a product is on eight channels, the quantity number has to be right on all eight, every day, forever — and every channel wants it in a different file format, keyed on a different identifier, in a different column.
I run this daily for a US building-products manufacturer: two Shopify storefronts, three retailer dropship feeds (two big-box US chains and one Canadian arm), Amazon Vendor Central and Amazon Seller Central, plus product-data syndication to six retail partners. Around 3,000 SKUs on the inventory side, 13,000+ variants on the consumer store.
This post is about the engineering of keeping that in sync.
What it looked like before
An 18 MB single-user Excel workbook with roughly 48 worksheet tabs, macro-driven, living on a virtual desktop. It computed sellable quantity per channel and emitted seven daily files. A human then uploaded each file to its destination by hand.
Every failure mode you would predict:
- File lock. One person at a time. If they are on holiday, inventory does not sync.
- No version control. The formula that broke last Tuesday is unrecoverable.
- No schedule. It runs when someone remembers.
- No audit trail. "Why did this SKU go to zero?" is unanswerable.
Worth saying: it worked. It ran a real business for years. The point of replacing it is not that it was stupid — it is that its correctness lived in one person's head.
Every channel has a different key and a different quantity column
This is the detail that makes multichannel sync annoying rather than trivial. The seven daily outputs:
- Shopify ×2 — keyed on
Variant ID, quantity inVariant Inventory Qty, XLSX, sheet must be namedProducts. - Three retailer dropship templates — keyed on
VENDOR SKU, quantity inQTY, legacy .xls, three instruction rows above the header (the real header is on row 4). - Amazon Seller — keyed on
item_sku, quantity inquantity, plusupdate_delete = PartialUpdate, XLSX with three reserved metadata rows. - Amazon Vendor — keyed on
SKU, quantity inAvailable units, CSV, latin-1 encoding.
Seven files, seven key columns, four file formats, one encoding gotcha. Any generic "sync tool" that promises to handle this without configuration is lying to you about at least three of them.
The pipeline that replaced it
Shape: INGEST → SQLite → TRANSFORM → OUTPUT.
Six tables replaced 48 tabs:
products— the SKU masterchannel_membership— which SKUs are offered on which channelinventory— quantity on handoos_status— out-of-stock flagsoverrides— force-in-stock and manual adjustmentsexceptions— SKUs suppressed on specific channels
Plus one computed view, channel_state.
The core rule set is deliberately tiny, and keeping it tiny is the whole design:
- A SKU is offered on a channel only if it is a member and not in exceptions.
sellable_qtystarts atqty_on_hand.- OOS and not force-in-stock → 0.
- Force-in-stock → keep on-hand regardless of OOS.
Four rules. When someone reports a wrong quantity, you can hold all four in your head while you debug.
SELECT p.sku,
CASE
WHEN o.force_in_stock = 1 THEN i.qty_on_hand
WHEN s.is_oos = 1 THEN 0
ELSE i.qty_on_hand
END AS sellable_qty
FROM channel_membership m
JOIN products p ON p.sku = m.sku
JOIN inventory i ON i.sku = m.sku
LEFT JOIN oos_status s ON s.sku = m.sku
LEFT JOIN overrides o ON o.sku = m.sku AND o.channel = m.channel
WHERE m.channel = ?
AND NOT EXISTS (
SELECT 1 FROM exceptions e
WHERE e.sku = m.sku AND e.channel = m.channel
);
The output trick: fill the template, do not rebuild it
This is the part I would most recommend copying.
The obvious approach is to generate each channel's file from scratch. The better approach: open the channel's real template file and update only the quantity column, keyed on that channel's own key, preserving everything else byte-for-byte.
Why it matters — the templates carry things you do not want to reimplement and cannot afford to lose: retailer instruction rows, Amazon's TemplateType=fptcustom metadata block, column ordering the parser depends on, and thousands of rows of static product data.
Verification was straightforward: run both systems on the same day and diff the outputs against the macro's own files. 16,654 rows preserved on the largest file, instruction rows intact, Amazon metadata block untouched.
Migration safety: shadow mode, then per-channel cutover
The old system stays the source of truth until each channel is individually proven.
- Run the new engine alongside the macro, on the same schedule.
- Compute what it would push. Do not push it.
- Diff against what the macro actually pushed. Log every divergence.
- After several consecutive days of matching, flip one channel to live.
- Repeat per channel.
This pattern — parallel run, then retire — shows up three separate times in this operation, and it has never once been the wrong call.
Being honest about what is not ported yet
The POC deliberately does not implement: reserved inventory, fuzzy SKU matching, old-SKU to new-SKU reassignment during a product-line transition, and the components gap check (do not sell a shower pan whose drain plate is out of stock).
Those four contain most of the remaining institutional knowledge. Naming them explicitly is what keeps a "we replaced the spreadsheet" claim honest — the spreadsheet still knows things the pipeline does not.
Stack choices, and what I refused to use
Python plus SQLite. openpyxl for XLSX, xlrd/xlutils/xlwt for the legacy .xls formats. Cron via a CI scheduler. Metabase or Streamlit for the dashboard. A chat webhook for alerts.
No Airflow. No Kubernetes. No Kafka. For 3,000 SKUs and seven daily files, that infrastructure is a second job, not a solution. SQLite goes to Postgres the day there is a second concurrent writer, and not a day before.
The Amazon feed that broke, and how it was fixed
A real incident worth generalising.
The daily Amazon Seller inventory upload started failing because Amazon retired the Listing Loader template the export targeted. The replacement demanded a Product Type on every SKU — which the daily feed has no business carrying, because it only pushes quantity and handling time.
The fix: switch the output to Amazon's still-supported Price & Quantity flat file, which requires no Product Type. All business logic stayed where it was; a thin formatting tab and an export macro were added on top.
And the part that matters: the old export was left running in parallel for a week as a safety net before retirement, with the change written into a change log.
The general rule — when a channel changes its spec under you, change the format layer, never the logic layer. If your quantity calculation has to be rewritten because Amazon changed a template, your architecture has a seam in the wrong place.
The target end state
Every one of the six manual uploads has an API equivalent:
- Shopify — Admin API
inventorySetQuantities, or a scheduled import-from-URL. - The dropship network — V3 REST API, OAuth bearer tokens, with a staging host to test against.
- Amazon Vendor — SP-API Vendor Direct Fulfillment Inventory.
- Amazon Seller — SP-API Feeds and Listings.
Proof order: one channel first, easiest auth first. Not all six at once, and not the hardest one first to "prove it works."
Google Merchant Center and the tag sprawl problem
One multichannel side effect that is almost universal: sales channel apps inject their own analytics tags.
The Google & YouTube channel installs a Merchant Center tag. If you also run GTM, and someone previously hardcoded gtag, you now have three. On the store I audited, that contributed to seven distinct Google tag loads on a single page — over a megabyte of transfer and ~1.7 seconds of CPU.
So when you add a channel, audit the storefront afterwards. The channel does more to your site than list your products on it.
Detecting the silent multichannel failure
Products flipped to Active with the sales channel never added. Active, in stock, and unbuyable. First audit: 164 across two stores, 35 with inventory, one with 5,469 units.
The catch: a CSV export's Published column reflects only the Online Store channel. A real "published to N of 8 channels" audit needs the Admin API publications query. If you syndicate widely, the CSV is telling you a comfortable half-truth.
The short version
- Every channel has its own key column and file format. Design for that, do not fight it.
- Keep the sellable-quantity rules small enough to reason about.
- Fill the real template rather than generating files from scratch.
- Shadow-run before cutover. Cut over one channel at a time.
- Separate business logic from output formatting, because channels change their formats and your logic should not care.
- Audit which channels each product is actually published to, with the API, not the CSV.
I build and run multichannel inventory and catalog pipelines across Shopify, Amazon, Google and national retail partners. See [Shopify development services](/shopify-expert) or [hire a Shopify developer](/hire-shopify-developer). Related reading: [the Matrixify field guide](/blog/matrixify-shopify-bulk-operations-guide) and [retail syndication to Lowe's, Home Depot and Menards](/blog/retail-syndication-salsify-lowes-home-depot).
Direct: [WhatsApp +55 11 98851-2788](https://wa.me/5511988512788) · [contato.matheusabrahao@gmail.com](mailto:contato.matheusabrahao@gmail.com)
Need a senior engineer who thinks like an operator?
I take on a small number of Shopify operations and senior engineering engagements each quarter. If your store needs catalog hygiene, technical SEO, performance, or marketing automation done right — let's talk.
Continue reading
Shopify Canonical URLs: The Duplicate Content Trap in Collections and Filters
How Shopify canonical URLs actually work, why collection-scoped product URLs and filter parameters multiply your index, and the theme-level fixes that hold.
Shopify Schema Markup: What Actually Earns Rich Results Now
FAQ rich results are gone. What still earns them: the structured_data Liquid filter, Product vs ProductGroup, and the required merchant listing fields.