The same product on a DesignMint Commerce store and on its Shopify shop, edited on one and synced to the other by a two-way sync
Channel integration

Shopify Sync: keeping two stores in step, both ways

Products, customers and orders synced both ways with Shopify. How we rebuilt an older sync, the eight bugs we found, and why the signatures kept failing.

DesignMint Commerce · Oct 14, 2026 · 5 min read
Ported
Sep 10, 2026
Per store
From Sep 22, 2026
Fixed along the way
8 inherited bugs

Plenty of shops already sell on Shopify and don’t want to give it up. They want DesignMint Commerce for the decoration side, meaning the studio, the pricing and the production files, and Shopify for the shop they’ve spent years building. That only works if the two stay in step: a product added in one shows up in the other, and an order placed in either one arrives in both.

Shopify Sync does that in both directions, for products, customers and orders. Each microstore connects its own Shopify shop.

What two-way sync really involves

One-way sync is copying. Two-way sync is a conversation, and conversations can go wrong in interesting ways. When DesignMint updates a product in Shopify, Shopify replies “this product changed”. If DesignMint then updates Shopify again because of that reply, you’ve built an echo that bounces forever. Most of the work in two-way sync is telling real changes apart from your own changes coming back.

DesignMintCommerce
ShopifyYour shop
  1. 1Update the productWe remember what we just sent
  2. 2“This product changed”Our own change, echoing back
  3. 3Sent by us under 10 seconds ago? Ignore it.No update goes back
  4. Without that check: update again…and the echo bounces forever
The fix is a short memory: we note each change we send, and if Shopify reports it back within 10 seconds, we ignore it, so the echo stops after one bounce.

Starting from older code, carefully

The sync started life in an earlier project built on an older version of Medusa (2.8.4, with zod 3). DesignMint Commerce runs on 2.18 with zod 4. We didn’t copy it across as it was. We read it line by line, and that close reading turned up eight bugs that had come along for the ride. Two of them were serious:

  • Prices were 100 times off in both directions. The old code assumed prices were stored in cents; Medusa 2 stores them in dollars. A $25 hoodie could become $2,500, or 25 cents. One conversion function now handles every price.
  • Incoming messages from Shopify weren’t being checked when no secret was set up. Now every message must carry a valid signature, and anything without one is turned away.

The rest were smaller: a value read outside the block where it was created, counts that always showed zero, and exported products losing their options, so Shopify answered every variant with “variant already exists”.

Some changes were about structure. Shopify login details moved into DesignMint Commerce’s encrypted integration store instead of a separate place of their own. And every action that changes data now runs through a workflow, a step-by-step process that can be retried and undone, because our build checks refuse to ship code that skips it.

Then every store needed its own Shopify

Twelve days after the port, microstores arrived, and each store needed to connect its own Shopify shop. The old code had one function to fetch “the” Shopify connection, with no way to say which store you meant. A product pushed from one store could have landed in another store’s Shopify.

The fix was small but strict. Every Shopify function now has to be told which store it is working for, and leaving that out is an error the code won’t build with, so it’s impossible to forget. A store’s “push all” only pushes that store’s catalogue, and incoming messages are matched to the right store by the Shopify shop’s address. A message from a shop we don’t recognise is turned away instead of guessed at.

Make the mistake impossible, not just unlikely. A required field you can’t skip is worth more than a comment that says “remember to pass this”.

Why the signatures kept failing

Shopify signs every message with a code made from the message itself and a secret both sides know. You make the same code on your side; if the two match, the message is genuine and nobody changed it on the way. Simple in theory. In practice, six commits in one day were titled “shopify webhook fix”.

There were two separate causes. First, we were turning the message into text before checking it. Converting raw bytes into text can quietly change a few of them, so our code no longer matched. The check now runs on the raw bytes, exactly as they arrived. Second, Shopify uses two different secrets depending on how a webhook was set up: ones registered through the API use the app’s client secret, while ones added by hand in Shopify’s settings use a separate webhook secret. We now try both. When a check fails, the log says whether it was the wrong secret or a damaged message, without ever printing the secret.

  1. Shopify signs every message
    Check the raw bytes, exactly as they arrived
    7b 22 69 64 22 3a …CodeMatches
    BytesTextCodeDidn’t match
  2. Two possible secrets
    Make the code with each secret, and try both
    • Webhook registered through the APIApp client secret
    • Webhook added by hand in Shopify’s settingsWebhook secret
  3. Compare with Shopify’s signature
    Accept it, or turn it away and say why
    MatchGenuine, and nobody changed it on the way.Accepted
    No matchTurned away. The log says which it was:reason: wrong secretreason: damaged messagethe secret is never printed
How every incoming message is checked now: on the raw bytes, with both possible secrets, and with a log that says why a check failed but never shows the secret.

The small rules that keep it calm

Two quiet windows
  • Echo windowFrom when we send a change to Shopify10 sOur own change coming back is ignored
  • Duplicate windowFrom when a message’s signature checks out60 sRepeats of that message are ignored
Repeats of a message are ignored for 60 seconds and our own echoes for 10. The duplicate window only starts once a message’s signature checks out, so a fake message can’t block a real one.
  • If a merchant pauses sync, we answer Shopify with “OK, ignored” instead of an error. An error would make Shopify keep retrying and eventually switch the connection off, over a setting the merchant chose on purpose.
  • Shopify used to be connectable for the whole platform and for each store. If both pointed at the same shop, every message was applied twice, so sync now works per store only.

What we learned

  • Read inherited code as if you wrote none of it. Eight bugs were hiding in code that “worked”.
  • Money needs one agreed unit, and one place that converts it.
  • Check signatures on the raw bytes, never on text you converted yourself.

Common questions

What does Shopify Sync keep in step?

Products, customers and orders, in both directions between DesignMint Commerce and Shopify.

Can each microstore use a different Shopify shop?

Yes. Every store connects its own shop, and each store only syncs its own catalogue.

What happens if I pause sync?

Messages from Shopify are acknowledged and ignored, so Shopify doesn’t disconnect, and nothing changes until you turn sync back on.

Filed under Channel integrationAll case studies
Case studies

Want it running
in your shop?

Book a call and we will walk through Stitch, lookbooks, mockups and microstores on your own products.