When I first started helping a friend manage their Shopify store, I thought the hardest part would be driving traffic. I was wrong. The real nightmare started after the checkout. Every Monday morning, I’d open my inbox to dozens of "Where is my order?" (WISMO) emails. Customers were anxious, and my friend was spending hours manually copying and pasting tracking numbers into reply templates. The default Shopify order status page felt like a dead end. It didn't reflect the store's branding, and it didn't give customers the real-time peace of mind they needed. I realized the post-purchase experience was really broken. So I decided to build something.
The Problem with the Post-Purchase Black Hole
The "Where Is My Order" (WISMO) problem is a significant hidden cost in e-commerce. When a customer clicks "buy," they enter a psychological black hole. They don't just want a receipt; they want to know their item is moving. If they have to dig through their email to find a tracking number, or if they land on a generic, unbranded status page, anxiety spikes.
This anxiety translates directly into support tickets. For a small store, handling WISMO tickets might just mean lost sleep. But as you scale, it means hiring more support staff, slower response times, and ultimately, lost repeat customers. Ignoring the post-purchase experience doesn't just annoy customers; it actively drains your operational budget and damages your brand reputation. You need a way to keep them engaged and informed without adding to your support queue.
Technical Approach: Building the Widget
To solve this, I needed to build a tracking widget that could be easily added to a Shopify store without requiring merchants to manually edit their theme code. Shopify’s Theme App Extensions and App Embed blocks were the ideal approach for this.
The goal was to create a lightweight, asynchronous widget that pulls in real-time tracking data and matches the store's theme. Here is the core Liquid and JavaScript snippet we use for the App Embed block:
{% 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>
Let's break down why it's structured this way. First, we use Liquid variables ({{ order.id }}, {{ order.name }}) to pass the specific order context directly into the DOM via data- attributes. This avoids making extra API calls on the client side just to figure out which order we are looking at.
Second, notice the JavaScript pattern. We dynamically create the <script> tag and set script.async = true. This is crucial for store performance. We don't want a third-party tracking widget to block the main thread or hurt the store's Core Web Vitals. By appending it to the document.head dynamically, it loads in the background. Once it loads, the onload callback initializes the widget, passing the DOM element ID as the container.
Of course, a frontend widget is useless without reliable backend data. To keep the tracking status up to date, the app relies on Shopify webhooks. When an order is fulfilled, Shopify sends a payload to our server. Security is paramount here, so we always verify the HMAC signature to ensure the webhook actually came from Shopify.
Here is a simplified version of the Node.js webhook handler:
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');
}
);
The verifyShopifyWebhook function takes the raw body, the HMAC header provided by Shopify, and your app's secret. It generates a hash and compares it to the header. If they don't match, we reject the request with a 401. If it's valid, we parse the JSON and upsert the fulfillment data into our database. This ensures that when the customer visits their order status page, the widget instantly has the correct carrier and tracking number to display.
Practical Results and Lessons Learned
Building this taught me a lot about balancing functionality with performance. What worked really well was the App Embed approach. Merchants could just toggle the widget on in their theme settings without touching a single line of Liquid code. The async script loading also kept Lighthouse performance scores high, which was a big win.
What surprised me the most was the psychological impact of the branded experience. Just by putting the store's logo, colors, and a clean visual timeline on the order status page, the tone of the support emails completely changed. Customers went from anxious to patient.
If I were to change anything, it would be my initial instinct to over-engineer the dashboard. I originally wanted to include complex analytics and marketing upsells right on the tracking page. I quickly realized that a focused, simple tracking experience was much better. Customers just want to know where their package is; don't distract them with too much noise.
Wrapping Up
I ended up packaging this exact setup into a tool called Parcelglance. It adds a branded, real-time tracking page to your Shopify store and helps reduce those endless WISMO tickets. You can install it easily via Shopify App Embed blocks, and it's completely free to try out. You can check it out and add the shipment tracking widget to your store at https://parcelglance.com.
Full disclosure: I'm the developer of Parcelglance. I built this because I needed it myself.
Top comments (0)