DEV Community

seller-mind
seller-mind

Posted on

How I Built a Shopify Tracking Widget to Stop WISMO Emails

When I first started helping a friend manage their Shopify store, I thought the hard part was getting sales. I was wrong. The real nightmare started after the checkout.

My inbox was flooded with "Where is my order?" (WISMO) emails. I found myself manually copying tracking numbers and pasting them into replies. The default Shopify order status page just gave customers a raw link to the carrier's site. It felt disjointed, unbranded, and frankly, a bit unprofessional.

So I decided to build something.

The Hidden Cost of WISMO

WISMO tickets are the silent killer of e-commerce margins. When a customer doesn't know where their package is, they don't blame the carrier; they blame your store.

If they have to navigate to a generic FedEx or UPS page, they are in an environment you don't control. If there's a minor delay, that generic page causes panic, resulting in a support ticket. Handling these tickets takes time, but the real cost is customer trust. A fragmented post-purchase experience makes your store feel like a one-time transaction rather than a cohesive brand.

The Technical Approach

To fix this, I needed a way to inject a branded tracking experience directly into the Shopify order status page. Crucially, it had to be done without requiring merchants to edit their theme code. Shopify's App Embed blocks were the perfect solution. They allow apps to inject HTML and JS into specific theme sections safely, surviving theme updates without breaking.

Here is the core Liquid and JavaScript snippet we use to render the widget. It safely passes the order context from Shopify's Liquid objects into the DOM, then asynchronously loads the tracking script.

{% 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

Notice how we use data- attributes to pass the order.id and order.name. This keeps the data scoped to the specific order without exposing it globally. The script is loaded asynchronously (script.async = true) so it doesn't block the rendering of the main order status page, preserving Core Web Vitals.

But a widget is useless without data. To get real-time tracking updates, we need to listen to Shopify's webhooks. When an order is fulfilled, Shopify sends a payload. We need to verify it and store the tracking info.

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

Security is paramount here. The verifyShopifyWebhook function uses the shared secret to ensure the request actually came from Shopify. Once verified, we extract the tracking number and carrier, upserting it into our database so the frontend widget can poll for updates.

Practical Results and Trade-offs

The App Embed approach worked beautifully because it completely eliminated the need for manual theme installations. Merchants just toggle a switch in their theme editor, and the widget appears.

What surprised me was the psychological impact of the UI. Even though the underlying tracking data was identical to what the carrier provided, presenting it in a clean, customizable interface that matches the store's theme helped reduce anxiety-driven emails. Customers recognize their own store's branding, which inherently builds trust. Another unexpected benefit was the reduction in support overhead. By keeping the customer on a branded page with real-time status updates, we intercepted the panic phase of the delivery journey. Instead of emailing support, they just refresh the tracking page.

If I were to rebuild the backend today, I would change the webhook handler. I'd swap the direct database upsert for a message queue. During high-volume sales events, processing webhooks synchronously and writing directly to the DB could become a bottleneck. Queuing the fulfillment events would allow for better rate limiting and resilience.

Wrapping Up

I packaged this entire system into a free tool called Parcelglance. It adds a branded, real-time tracking widget to your Shopify store to help keep your customers informed and your inbox quiet. 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)