Shopify Sync
Sync customers, orders, and products from Shopify.
@rx-ventures/medusa-plugin-shopify-sync
Medusa v2 plugin that syncs customers, orders, products, and discounts from a Shopify store into Medusa, with manual triggers (admin UI buttons) and inbound webhook receivers.
Compatibility: Medusa v2.13.x, Node >= 20.
What it does
Domain Manual sync Webhook receivers Customers ✅ paginated ✅ Copy to clipboardcustomers/{create,update,delete,enable,disable} Orders ✅ paginated ✅ Copy to clipboardorders/{create,updated,cancelled,paid,fulfilled,partially_fulfilled,edited} + Copy to clipboardrefunds/create Products ✅ images downloaded from Shopify CDN and re-uploaded through Medusa's File module ✅ Copy to clipboardproducts/{create,update,delete} Discounts ✅ paginated manual only by design Inventory — ✅ Copy to clipboardinventory_levels/{update,connect,disconnect} Fulfillments — ✅ Copy to clipboardfulfillments/{create,update} — runs reconcile on parent order, writes fulfillment chain Draft orders — ⚠️ logged only
Every webhook delivery — successful or failed — writes a row to Copy to clipboardshopify_webhook_log. The admin Shopify webhook logs page (top-level, separate from Settings) shows them with filters by entity / topic / action / status code, auto-refreshes every 5 seconds, and renders clickable chips that open the affected Medusa entity.
Install
1yarn add @rx-ventures/medusa-plugin-shopify-sync
Generate an encryption key (used to encrypt the Shopify access token at rest with AES-256-GCM):
1openssl rand -hex 32
Add it to your Medusa application's environment:
1SHOPIFY_SYNC_ENCRYPTION_KEY=<the 64-char hex you just generated>
⚠️ Use the same value across deploys. If it changes, the saved Shopify token in your DB becomes undecryptable and you'll need to re-paste it once via the admin UI.
Register the plugin in your Copy to clipboardmedusa-config.ts:
123456789101112131415import { defineConfig } from "@medusajs/framework/utils"module.exports = defineConfig({// ...plugins: [{resolve: "@rx-ventures/medusa-plugin-shopify-sync",options: {encryption_key: process.env.SHOPIFY_SYNC_ENCRYPTION_KEY,// shopify_api_version: "2026-01" // override Shopify Admin GraphQL version// webhook_base_url: process.env.MEDUSA_BACKEND_URL // for webhook auto-registration},},],})
Run migrations:
1yarn medusa db:migrate
This creates two tables in your Medusa DB:
- Copy to clipboard
shopify_config— singleton row holding the (encrypted) Shopify credentials, webhook secret, and last-sync timestamps. - Copy to clipboard
shopify_webhook_log— one row per inbound webhook delivery for the admin log page.
Usage
Start your Medusa app and open the admin dashboard.
1. Configure the connection
Navigate to Settings → Shopify Sync.
- Paste your store URL (bare domain or full URL — both work).
- Paste your Shopify Admin API access token. Required scopes: Copy to clipboard
read_customers, write_customers, read_orders, write_orders, read_products, write_products, read_discounts, write_webhooks. - Click Save, then Test connection to verify.
2. Run the manual sync
From the same Settings page, run each entity in this recommended order:
- Customers — populates Medusa customers from Shopify (email-dedup, address sync, metafield flatten).
- Products — downloads images from Shopify and re-uploads to your Medusa file service. Variant matching by SKU.
- Discounts — creates Medusa promotions under a single Copy to clipboard
shopify_discounts_synccampaign. - Orders — full historical import. Customers, orders' addresses, line items, summary, payment chain, and discount line-item adjustments. Uses direct SQL writes for the order tables since Medusa has no public Admin API for "import historical order with payments + adjustments".
For Customers and Orders the section exposes batch controls (max batches + items per batch) so you can dry-run on a small slice before a full import.
3. Register webhooks
In Settings → Shopify Sync → Webhook subscriptions on Shopify, paste your public Medusa URL (your production URL or an ngrok tunnel for local dev) and click Register all defaults. The plugin appends each topic's path automatically:
Topic family URL appended Copy to clipboardCUSTOMERS_* Copy to clipboard/hooks/shopify/customers Copy to clipboardORDERS_* + Copy to clipboardREFUNDS_CREATE Copy to clipboard/hooks/shopify/orders Copy to clipboardPRODUCTS_* Copy to clipboard/hooks/shopify/products Copy to clipboardFULFILLMENTS_* Copy to clipboard/hooks/shopify/fulfillments Copy to clipboardINVENTORY_LEVELS_* Copy to clipboard/hooks/shopify/inventory Copy to clipboardDRAFT_ORDERS_* Copy to clipboard/hooks/shopify/draft-orders
This is additive only — never deletes existing subscriptions on the same store, so other integrations are safe.
4. Watch webhook activity
Open Shopify webhook logs (top-level admin page). Auto-refreshes every 5 s. Filter by:
- Entity (customers / orders / products / fulfillments / inventory / draft_orders)
- Topic (every supported topic)
- Action (created / updated / deleted / skipped / errored / unauthorized)
- Status code (200 / 400 / 401 / 404 / 500)
Each row shows the topic, action, duration, payload summary, error message, and clickable chips that open the affected Medusa entity in admin (Copy to clipboardOpen order ord_…, Copy to clipboardOpen customer cus_…, etc.). Fulfillment log rows link the parent order.
Configuration reference
Plugin options
Option Type Default Purpose Copy to clipboardencryption_key Copy to clipboardstring Copy to clipboardprocess.env.SHOPIFY_SYNC_ENCRYPTION_KEY 64-char hex (32 bytes) AES-256-GCM key for the at-rest Shopify access token. Required (one of plugin option or env). Copy to clipboardshopify_api_version Copy to clipboardstring Copy to clipboard"2026-01" Override Shopify Admin GraphQL API version. Copy to clipboardwebhook_base_url Copy to clipboardstring Copy to clipboardMEDUSA_BACKEND_URL env Public URL Shopify can reach Copy to clipboard/hooks/shopify/... at. Used for webhook auto-registration.
Required env vars in your Medusa application
Var Required Purpose Copy to clipboardSHOPIFY_SYNC_ENCRYPTION_KEY yes The 64-char hex key. Same value across deploys. Copy to clipboardMEDUSA_BACKEND_URL recommended Public URL — used as the default base for webhook registration.
The store URL and Shopify access token are saved through the admin UI (encrypted at rest) — not via env vars — so you can rotate without redeploying.
Admin API surface
Method Path Purpose Copy to clipboardGET Copy to clipboard/admin/shopify-sync/config Redacted config (no plaintext token) Copy to clipboardPOST Copy to clipboard/admin/shopify-sync/config Save store URL / token / enabled / rotate webhook secret Copy to clipboardPOST Copy to clipboard/admin/shopify-sync/connection-check Hits Shopify Copy to clipboard{ shop { name } } Copy to clipboardGET Copy to clipboard/admin/shopify-sync/webhooks Current Shopify subscriptions + curated topic list Copy to clipboardPOST Copy to clipboard/admin/shopify-sync/webhooks Create one subscription Copy to clipboardDELETE Copy to clipboard/admin/shopify-sync/webhooks/:numericId Delete one Copy to clipboardPOST Copy to clipboard/admin/shopify-sync/webhooks/register-all Bulk register the default set (additive only) Copy to clipboardPOST Copy to clipboard/admin/shopify-sync/customers/sync Manual sync — body Copy to clipboard{ cursor?, batchSize? } Copy to clipboardPOST Copy to clipboard/admin/shopify-sync/orders/sync Same shape Copy to clipboardPOST Copy to clipboard/admin/shopify-sync/products/sync Same shape Copy to clipboardPOST Copy to clipboard/admin/shopify-sync/discounts/sync Same shape Copy to clipboardGET Copy to clipboard/admin/shopify-sync/webhook-logs Paginated, filterable by Copy to clipboardentity / topic / action / status_code / limit / offset
Public webhook surface
These routes are public (no admin auth) and are what you register in Shopify:
123456POST /hooks/shopify/customersPOST /hooks/shopify/ordersPOST /hooks/shopify/productsPOST /hooks/shopify/fulfillmentsPOST /hooks/shopify/inventoryPOST /hooks/shopify/draft-orders
Each route routes by the Copy to clipboardX-Shopify-Topic header, so the same URL accepts every topic in its family.
Database compatibility
The plugin's order import path uses raw SQL (necessary because Medusa has no public Admin API for historical order import). It works on any Postgres compatible with Medusa, including managed services. SSL is auto-enabled when the connection string contains Copy to clipboardsslmode=require, Copy to clipboardneon.tech, or Copy to clipboardsupabase.co.
⚠️ Schema-tied: re-validate after any major Medusa version upgrade. Minor / patch upgrades have been stable.
License
MIT — see LICENSE.

