When I first started helping a friend manage their Shopify store, I genuinely thought the hard part was getting the initial sales. I was wrong. The real headache started three days after a product shipped. My inbox was suddenly flooded with "Where is my order?" (WISMO) emails. Customers were anxious, and I was spending hours every day copy-pasting tracking numbers from carrier websites just to reply to support tickets. It was exhausting, entirely manual, and completely unscalable. So I decided to build something.
The Post-Purchase Blind Spot
The post-purchase experience is often an afterthought in e-commerce. Store owners spend significant time and budget on customer acquisition, but once the order is placed, the customer is frequently left in the dark until the physical package arrives on their doorstep.
This communication gap creates unnecessary anxiety. When customers can't easily find their tracking information, they default to emailing support. The cost of ignoring this isn't just the time spent answering repetitive emails; it's the erosion of customer trust. A frustrated customer who feels ignored is much less likely to become a repeat buyer.
From a technical and operational standpoint, handling a high volume of support tickets manually means your team isn't focusing on growth or product development. We needed a way to keep customers informed automatically, directly on the store, without building a massive, complex custom backend from scratch. The goal was to create a seamless, branded experience that answers the customer's primary question before they even think to ask it.
The Technical Approach
I needed a solution that was lightweight, didn't negatively impact storefront performance, and could be easily embedded by merchants without requiring them to edit complex Liquid files. I decided to leverage Shopify's App Embed blocks. This architecture allows the widget to be injected directly into the theme securely and cleanly.
Frontend Widget Injection
For the frontend, I created a snippet that merchants can add to their order status page. It uses Shopify's Liquid templating language to pass the necessary order context to the widget, and 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>
There are a few key decisions here. First, we use data-* attributes to pass the order.id and order.name from Liquid to the DOM. This keeps the JavaScript clean and avoids inline script execution issues with strict Content Security Policies. Second, the script is loaded with script.async = true. This is critical for Core Web Vitals; we never want a third-party widget to block the main thread and delay the rendering of the primary order status content.
Backend Webhook Handling
The frontend widget needs data to display. To get real-time tracking status, the backend needs to know when an order is fulfilled. This is where Shopify Webhooks come in. Handling webhooks securely is a common stumbling block, so I want to highlight the HMAC verification process.
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');
}
);
Shopify requires all webhooks to be verified using an HMAC SHA256 hash to ensure the payload actually came from Shopify and wasn't tampered with. Notice that we use express.raw() to capture the raw body. If you use the standard JSON body parser, the raw byte buffer is lost, and your HMAC hash calculation will fail, resulting in rejected webhooks. Once verified, we parse the JSON and upsert the tracking data into our database so the frontend widget can fetch it later.
Practical Results and Reflections
The biggest win was seeing the immediate drop in support tickets. By giving customers a branded, real-time tracking page, they stopped emailing us and started checking the widget directly.
What surprised me during development was how much the visual customization mattered. When the tracking widget matched the store's theme and branding, customers engaged with it much more than when it looked like a generic, bolted-on third-party tool. Trust is heavily influenced by visual consistency.
If I were to change one thing about the architecture, I'd probably optimize the initial script payload further. While keeping the script async mitigates most performance concerns, I'm always looking for ways to reduce the DOM size and JavaScript execution time. It's a delicate balance between adding rich features and keeping the storefront lightweight.
Wrapping Up
I eventually packaged this solution into Parcelglance, a Shopify app that adds a branded tracking page to your store to help reduce WISMO tickets. It handles the real-time carrier updates and provides a customizable widget that matches your store's theme.
If you're dealing with post-purchase support overload and want to improve your customer experience, you can try the free Shipment Tracking Widget at parcelglance.com. It's designed to be easy to install via Shopify App Embed blocks, requiring no manual code edits to get started.
Full disclosure: I'm the developer of Parcelglance. I built this because I needed it myself.
Top comments (0)