DEV Community

seller-mind
seller-mind

Posted on

How I Built a Custom Shopify Tracking Widget to Reduce WISMO

When I first started managing a Shopify store, I genuinely thought the hard part was getting the first sale. I was wrong. The real headache started right after checkout.

My inbox was suddenly flooded with "Where is my order?" (WISMO) emails. I was spending hours every single day just copying and pasting tracking numbers into support replies, playing a glorified human tracking-number lookup service. It was draining my time, killing my productivity, and frankly, frustrating my customers who just wanted a quick update.

I realized I was spending more time managing post-purchase anxiety than actually growing the business. So I decided to build something.

The Hidden Cost of the Post-Purchase Gap

The post-purchase experience is often the most neglected part of e-commerce architecture. You spend weeks optimizing your checkout flow, A/B testing buttons, and reducing friction, but once the customer pays, they are essentially left in the dark until the physical package arrives.

This creates a massive anxiety gap. When customers don't know where their stuff is, they email you. WISMO tickets are the silent killer of e-commerce margins. They don't just cost you support overhead; they erode trust. If a customer has to chase you for an update, they aren't coming back for a second purchase. The cost of ignoring this experience isn't just the time spent replying to emails; it's the lost lifetime value of a frustrated customer.

I needed a way to give customers real-time tracking updates without forcing them to leave my store, and without requiring me to manually intervene.

The Technical Approach: Webhooks and App Embeds

To solve this, I needed a two-part architecture. First, I needed a secure way to capture fulfillment data from Shopify the moment a carrier scanned a package. Second, I needed a way to display that data on the customer's order status page without forcing merchants to edit their theme code.

Part 1: Securely Capturing Fulfillment Data

Relying on the Shopify Admin API to poll for order updates is inefficient and eats up your API rate limits. The right approach is to use webhooks. However, webhooks require strict validation to ensure the payload actually came from Shopify.

Here is the Express.js handler I built to listen for fulfillment updates and verify the HMAC signature:

const crypto = require('crypto');

function verifyShopifyWebhook(rawBody, hmacHeader, secret) {
  const hash = crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('base64');
  return hash === hmacHeader;
}

// Handle fulfillment update webhook
app.post('/webhooks/orders/fulfilled',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const verified = verifyShopifyWebhook(
      req.body,
      req.headers['x-shopify-hmac-sha256'],
      process.env.SHOPIFY_WEBHOOK_SECRET
    );

    if (!verified) return res.status(401).send('Invalid');

    const order = JSON.parse(req.body.toString('utf8'));
    // Store fulfillment data for tracking lookup
    await db.fulfillments.upsert({
      orderId: order.id,
      trackingNumber: order.fulfillments[0]?.tracking_number,
      carrier: order.fulfillments[0]?.tracking_company,
      status: 'shipped',
    });

    res.status(200).send('OK');
  }
);
Enter fullscreen mode Exit fullscreen mode

The key here is using express.raw() to capture the raw body before JSON parsing, which is required for the HMAC SHA256 verification to match Shopify's signature. Once verified, I upsert the tracking data into my database so the frontend widget can query it later.

Part 2: Injecting the Widget via App Embeds

Initially, I thought about just giving merchants a snippet of Liquid code to paste into their order-status.liquid file. But theme updates would wipe out their changes, and it was a terrible user experience.

Shopify's App Embed blocks are the modern solution for this. They allow apps to inject UI elements seamlessly into the theme editor. Here is the core Liquid and JavaScript snippet that powers the widget injection:

{% comment %}
  Renders shipment tracking widget on order status pages
{% endcomment %}

<div id="parcelglance-widget" 
     data-order-id="{{ order.id }}"
     data-order-number="{{ order.name }}"
     data-store-name="{{ shop.name }}"
     style="min-height: 200px;">
</div>

<script>
(function() {
  const script = document.createElement('script');
  script.src = 'https://cdn.parcelglance.com/widget.js';
  script.async = true;
  script.onload = function() {
    Parcelglance.init({
      orderId: document.getElementById('parcelglance-widget').dataset.orderId,
      orderNumber: document.getElementById('parcelglance-widget').dataset.orderNumber,
      storeName: document.getElementById('parcelglance-widget').dataset.storeName,
      container: 'parcelglance-widget',
    });
  };
  document.head.appendChild(script);
})();
</script>
Enter fullscreen mode Exit fullscreen mode

There are a few technical decisions worth highlighting here. First, we use Liquid variables ({{ order.id }}) to pass the necessary context directly into the DOM as data attributes. This keeps the JavaScript clean and avoids inline scripting issues.

Second, the script is loaded asynchronously using an Immediately Invoked Function Expression (IIFE). This ensures that fetching the widget script doesn't block the main thread or delay the rendering of the rest of the order status page. We set a min-height on the container to prevent layout shift (CLS) when the widget finally renders.

Practical Results and Trade-offs

Moving to an App Embed architecture was a massive improvement for distribution. Instead of asking merchants to dig into their theme code, they just toggle a switch in the Shopify theme editor. It made the installation process completely frictionless.

What surprised me the most was how much the branded experience mattered. Even though the underlying tracking data was identical to what they would get from a generic carrier email, putting it in a customizable widget that matched the store's theme made customers feel like they were still "in the store." It transformed a utility into a brand touchpoint.

If I were to rebuild the backend today, I would lean even heavier into server-side processing for carrier API polling. Initially, I had the frontend widget polling my backend for updates, which caused unnecessary database reads. Shifting to a push-based model where the backend listens to carrier webhooks and only serves the latest cached state to the frontend made the widget load significantly faster.

Taming the WISMO Beast

Building this internally completely changed how I managed the store. The "Where is my order?" emails dropped to near zero, freeing up hours of my week.

Since this solved such a universal problem for Shopify merchants, I packaged my solution into a free tool called Parcelglance. It's a Shopify shipment tracking widget that handles the real-time carrier syncing and gives you a customizable, branded widget to drop right into your store via App Embeds.

If you're dealing with this exact problem and want to give your customers a better post-purchase experience, you can check it out and install it for free at parcelglance.com.


Full disclosure: I'm the developer of Parcelglance. I built this because I needed it myself.

Top comments (0)