When I first launched my Shopify store, I thought the hardest part would be getting the first sale. I was wrong. The hardest part was what happened after the sale. A few weeks in, my inbox started filling up. Not with praise, but with variations of the same question: "Where is my order?"
I was spending hours every day manually checking carrier websites and copy-pasting tracking links to customers. I was struggling to keep up, and my actual customers were getting frustrated by the delayed responses. It felt like I was running a customer support agency instead of an e-commerce brand.
So I decided to build something.
The Problem with WISMO
In e-commerce, this phenomenon is known as WISMO (Where Is My Order). It is the single most common support ticket for any online store.
The problem with WISMO isn't just that it is annoying. It is that it is incredibly costly. Every minute spent looking up a tracking number is a minute not spent on product development, marketing, or improving the store. Furthermore, if customers cannot easily see their package status, they panic. That panic translates into support tickets, which delays responses to actual issues like damaged goods or lost packages.
Ignoring the post-purchase experience means you are handing over control of your brand to a generic carrier tracking page. You lose the opportunity to keep the customer engaged, and you burn out your support team in the process.
Technical Approach
To solve this, I needed a way to inject a branded tracking widget directly into the Shopify order status page without requiring merchants to manually edit their theme code. Shopify's App Embed blocks were the perfect solution for this.
The architecture required two main components: a backend to handle order fulfillment data securely, and a frontend widget to display that data to the customer.
First, let's look at the backend. When an order is fulfilled, Shopify sends a webhook. We need to listen to this webhook and store the tracking data. The most critical part of handling Shopify webhooks is verifying the HMAC signature to ensure the request actually came from Shopify and wasn't spoofed.
Here is how I set up the webhook handler in Node.js using Express:
const crypto = require('crypto');
const express = require('express');
const app = express();
function verifyShopifyWebhook(rawBody, hmacHeader, secret) {
const hash = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('base64');
return hash === hmacHeader;
}
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');
}
);
Notice that we use express.raw to get the raw body. This is required because the HMAC signature is calculated against the raw JSON string, not the parsed object. If you parse it first, the stringification might alter whitespace or key ordering, breaking the hash verification.
On the frontend, we use a Shopify App Embed block. This allows the widget to be toggled on or off via the theme editor. We pass the necessary order data via Liquid variables into data attributes, and then load the widget script asynchronously.
{% 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>
This approach keeps the theme clean. The Liquid variables securely pass the order context, and the JavaScript handles the asynchronous loading of the tracking UI, ensuring we don't block the main thread or slow down the page render.
Practical Results
Building this out taught me a lot about the nuances of e-commerce logistics. What worked really well was the App Embed approach. Merchants loved that they didn't have to touch a single line of Liquid code to get the tracking page up and running.
What surprised me was how much the visual design of the tracking page mattered. I initially thought customers just wanted the raw data. But by matching the widget's styling to the store's theme, the post-purchase experience felt like a natural continuation of the brand, rather than a jarring redirect to a carrier's website.
If I were to change one thing, it would be my initial approach to fetching carrier data. Early on, I used a cron job to poll carrier APIs every hour. This was a mistake. It wasted server resources and quickly hit API rate limits. Refactoring to rely entirely on Shopify's fulfillment webhooks, combined with on-demand fetching only when a customer actually views the page, was a much more efficient architecture.
Wrapping Up
If you are dealing with WISMO tickets on your Shopify store and want to improve your post-purchase experience, I have packaged this tracking solution into a free tool called Parcelglance. It adds a customizable, real-time shipment tracking widget to your store, helping to reduce support tickets and keep customers engaged.
You can check out the free Shipment Tracking Widget at parcelglance.com.
Full disclosure: I'm the developer of Parcelglance. I built this because I needed it myself.
Top comments (0)