Ashiq's Portfolio
Toggle sidebar
Syncing WooCommerce and Shopif...
Article
Updated Jul 13, 2026 6 min read

Syncing WooCommerce and Shopify with Laravel

Laravel WooCommerce Shopify API Integration
Syncing WooCommerce and Shopify with Laravel

Running a store on WooCommerce and Shopify at the same time sounds simple until you try to keep them in agreement. Products drift, stock counts disagree, and orders land in two places that never talk to each other. As a Full Stack Developer in Dubai, this is one of the most common integration problems I solve for e-commerce clients β€” so here is how I approach it with Laravel, in enough detail that you could build it yourself.

Why a middleware, not a plugin

You can bolt a plugin onto each platform, but plugins live inside someone else's release cycle and break on updates. A small Laravel middleware service sitting between the two stores gives you one place to own the logic, log every sync, and recover from failures. It becomes the single source of truth.

The shape is straightforward:

WooCommerce ──webhook──▢ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ──REST──▢ Shopify
                         β”‚   Laravel service   β”‚
Shopify ─────webhook──▢  β”‚  normalise β†’ queue  β”‚ ──REST──▢ WooCommerce
                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
                     mapping table + sync log
  • WooCommerce and Shopify each fire webhooks on product, inventory, and order events.
  • A Laravel app receives those webhooks, verifies the signature, normalises the payload, and queues a job.
  • A queued job pushes the change to the other platform through its REST API.

Every event passes through one funnel, which means one place to log, one place to retry, and one place to debug when a client asks why a stock count looks wrong.

Verify webhooks before you trust them

Both platforms sign their webhooks, and both signatures must be checked before anything touches your queue. Shopify sends an HMAC-SHA256 of the raw body in the X-Shopify-Hmac-Sha256 header:

public function verifyShopify(Request $request): bool
{
    $calculated = base64_encode(hash_hmac(
        'sha256',
        $request->getContent(),
        config('services.shopify.webhook_secret'),
        true,
    ));

    return hash_equals($calculated, $request->header('X-Shopify-Hmac-Sha256', ''));
}

WooCommerce does the same with X-WC-Webhook-Signature. Two details bite people here: you must hash the raw request body (not the parsed array), and you must compare with hash_equals() to avoid timing attacks. Reject anything that fails verification with a 401 and log it β€” a burst of failed signatures usually means a secret was rotated on one side and not the other.

Normalising two very different APIs

WooCommerce and Shopify model the same concepts differently β€” variants, SKUs, and inventory locations rarely line up one-to-one. The trick is to map both into a neutral internal representation first:

  • Match products by SKU, never by internal ID.
  • Treat inventory as a delta where possible, not an absolute overwrite.
  • Store an external-ID mapping table so each side knows the other's identifiers.

The mapping table is the heart of the whole service. Mine looks like this:

Schema::create('product_mappings', function (Blueprint $table) {
    $table->id();
    $table->string('sku')->unique();
    $table->unsignedBigInteger('woo_product_id')->nullable();
    $table->unsignedBigInteger('shopify_product_id')->nullable();
    $table->unsignedBigInteger('shopify_inventory_item_id')->nullable();
    $table->timestamp('last_synced_at')->nullable();
    $table->timestamps();
});

When a webhook arrives, the handler resolves the SKU to a mapping row, then knows exactly which record to update on the other platform. If no mapping exists, the product gets created on the target side and the row is filled in β€” which means the same pipeline handles both "new product" and "product updated" without special cases.

Idempotency and webhooks

Webhooks get delivered more than once β€” that is a documented behaviour of both platforms, not an edge case. Every handler has to be idempotent: processing the same event twice must not double an order or halve the stock. I key each job on the event ID and short-circuit if it has already run:

public function handle(): void
{
    if ($this->alreadyProcessed($this->eventId)) {
        return;
    }

    $this->syncToTarget();
    $this->markProcessed($this->eventId);
}

Laravel's ShouldBeUnique interface adds a second layer: if the same event is queued twice in quick succession, only one job instance runs at a time.

There's a subtler failure mode worth designing for: the echo loop. Your service updates Shopify, Shopify fires a "product updated" webhook back at you, and your service dutifully pushes that update to WooCommerce β€” which fires its own webhook, forever. I break the loop by recording a fingerprint of every change the service itself makes; when a webhook arrives that matches a fingerprint written in the last few minutes, it is an echo and gets dropped.

Rate limits and retries

Shopify enforces its API limits with a leaky-bucket algorithm and will return 429 responses with a Retry-After header when you push too fast. A bulk product import can hit that ceiling in seconds. Laravel's queue makes the polite behaviour easy:

public function backoff(): array
{
    return [10, 60, 300];
}

public function retryUntil(): DateTime
{
    return now()->addHours(6);
}

Transient failures retry with increasing delays and give up after a sensible window. Anything that exhausts its retries lands in the failed_jobs table and triggers a Telegram alert β€” the store owner never has to discover a sync failure by noticing wrong numbers.

Common pitfalls

  1. Trusting absolute stock values. If both sides send "stock is now X" and events arrive out of order, the older value can overwrite the newer one. Prefer deltas, or attach timestamps and reject stale updates.
  2. Matching on product titles. Titles get edited; SKUs shouldn't. If the client's catalog has missing or duplicate SKUs, fix that before building the sync β€” no code survives dirty keys.
  3. Skipping signature verification in development and forgetting to turn it back on. Make it impossible to disable in production via config.
  4. Processing webhooks synchronously. Shopify expects a fast 200 response; do the real work in a queued job, always.
  5. Forgetting currency. A Dubai store selling in AED with a Shopify presence priced in USD needs an explicit conversion policy, not an assumption.

Lessons learned

  1. Log everything. When a client asks "why does this product show 3 in stock here and 5 there?", the audit trail is your answer.
  2. Fail loudly, retry quietly. Surface hard failures to a Telegram/Slack alert, but let transient API errors retry with backoff.
  3. Rate limits are real. Shopify's API will throttle you; batch and queue accordingly.

A reliable two-way sync isn't glamorous, but it's the difference between a store owner trusting their numbers and chasing ghosts in a spreadsheet.

FAQ

Can this sync more than two stores?
Yes. Because everything is normalised into an internal representation first, adding a third channel (say, a Salla or Amazon storefront) means writing one new adapter, not rewriting the pipeline.
How long does an integration like this take?
A focused two-way product/inventory/order sync is typically two to four weeks including testing against real catalogs β€” most of the calendar time goes into handling the client's data quirks, not the happy path.
Does the middleware need its own server?
It runs happily on a small VPS or even a well-configured self-hosted box. It is a queue worker and a webhook endpoint β€” CPU needs are modest; reliability and monitoring matter far more than horsepower.
What about syncing customers and prices?
Same pattern: map by a stable key (email for customers), normalise, queue, push. Prices deserve extra care around currency and tax-inclusive vs tax-exclusive amounts β€” agree the rules with the store owner first.

Need a WooCommerce ↔ Shopify integration, or a custom Laravel build in Dubai, UAE? Get in touch.