DEV Community

seller-mind
seller-mind

Posted on

How I Engineered a Shopify App Embed to Stop WISMO Tickets

When I first started managing fulfillment for a Shopify store, I quickly realized that the checkout process was only half the battle. The real headache started the moment the customer clicked "Place Order." For the first few weeks, my inbox was flooded with a single, repetitive question: "Where is my order?"

I was spending hours every day manually copying tracking numbers from carrier websites and pasting them into email replies. It was exhausting, unscalable, and frankly, a terrible use of my time as a developer. The post-purchase experience was a complete black hole. So I decided to build something.

The Hidden Cost of WISMO

In e-commerce, WISMO (Where Is My Order) tickets are a silent drain on resources. It’s not just about the minutes spent typing out email responses. Every WISMO ticket represents a moment of customer anxiety. When buyers can't easily check their order status, they lose confidence in your brand.

If you ignore the post-purchase experience, your support queue gets clogged with repetitive logistical queries. This leaves you with less time to focus on actual product improvements or genuine customer service issues. Furthermore, a disjointed tracking experience—where customers are bounced between your store and a generic carrier website—breaks the brand immersion you worked hard to build during checkout.

The Technical Approach

To solve this, I needed to build a branded tracking widget that lived directly on the Shopify order status page. My initial thought was to inject code directly into the theme's Liquid files. However, theme edits are notoriously fragile; a single theme update could wipe out your custom code.

The better architectural decision was to use Shopify App Embed blocks. This allows the widget to be injected dynamically without touching the core theme files, making it resilient to merchant theme changes.

Here is the core Liquid and JavaScript for the App Embed block. We pass the order context from Liquid into the DOM, then asynchronously load 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

By using script.async = true, we ensure that fetching the widget logic doesn't block the rendering of the main order status page. The min-height on the container prevents Cumulative Layout Shift (CLS) while the widget loads, keeping Core Web Vitals healthy.

When the frontend initializes, it makes a lightweight API call to our backend using the orderId passed via the data attributes. But a frontend widget is useless without data. To get the tracking information, we can't just rely on the frontend to poll carrier APIs directly—that's a security risk and a performance bottleneck.

Instead, we need a backend listening to Shopify Webhooks. When an order is fulfilled, Shopify sends a payload. We need to verify this payload to ensure it actually came from Shopify and hasn't been tampered with. Here is how I structured the Express.js webhook handler to securely capture fulfillment data:

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

Using express.raw() is crucial here because the HMAC verification requires the exact raw byte payload that Shopify sent. If you parse it to JSON first, the hash won't match. Once verified, we safely extract the tracking number and carrier, storing it in our database for the frontend widget to query.

Practical Results and Lessons Learned

Moving to this App Embed architecture was a massive win for maintainability. Because the widget is decoupled from the theme, I never have to worry about a merchant updating their theme and accidentally breaking their tracking page.

What surprised me most was the impact on the actual user experience. Just having a centralized, branded tracking page kept customers on the store's domain longer, rather than bouncing them to a third-party carrier site. It felt like a natural, cohesive extension of the checkout flow, which greatly improved post-purchase engagement.

If I were to start over, I would change how I initially handled carrier API polling. I originally tried to fetch tracking statuses on-demand from the frontend. It was slow and quickly hit rate limits. Shifting to a webhook-driven model, where we only update the database when Shopify tells us an order is fulfilled, was much more efficient. I'd also invest more time early on in building out a robust retry mechanism for failed webhook deliveries, as Shopify will eventually drop them if your server times out or returns a 5xx error.

Wrapping Up

If you are dealing with similar WISMO headaches and want to improve your post-purchase experience without wrestling with fragile theme code, I packaged this solution into a free tool. You can install the Parcelglance Shipment Tracking Widget directly via Shopify App Embed blocks to get a branded, real-time tracking page up and running quickly.


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

Top comments (1)

Collapse
 
billyprompts profile image
Billy Simmons •

Curious what cut WISMO volume the most for you: showing tracking on the order page, or the proactive “shipped + ETA” email? For a small shop I helped, the biggest win wasn’t a fancier tracker — it was one plain status block (Ordered → Packed → Shipped → Out for delivery) plus the carrier link only after a real scan existed. That alone stopped most “did it ship yet?” tickets before day 3.