When I first started helping a friend manage their Shopify store, I thought the hardest part would be driving traffic. I was wrong. The hardest part was the post-purchase experience. Specifically, the endless flood of "Where is my order?" (WISMO) emails.
Every time a package was delayed by a carrier, my inbox exploded. I found myself manually copying tracking numbers from Shopify and pasting them into email replies. It was exhausting, unscalable, and frankly, a terrible customer experience.
I looked for existing apps, but they often required heavy theme modifications or didn't allow enough styling control to match the store's brand. I just wanted a clean, customizable widget that matched the store's theme and kept customers informed without them having to leave the site.
So I decided to build something.
The Hidden Cost of WISMO
The WISMO problem is a silent drain on small e-commerce stores. It’s not just about the annoyance of reading the same email ten times a day. It’s about the hidden operational costs.
Every WISMO ticket requires support time. If you are paying a virtual assistant or an agency for support, those minutes add up in dollars. More importantly, it fractures trust. When a customer buys from you, they expect a seamless experience. If they have to hunt for their tracking info on a generic carrier website that doesn't even show your store's name, the post-purchase experience feels disjointed.
Standard Shopify order status pages just provide a plain text link to the carrier. For major carriers, this works, but for regional ones, the tracking pages are often confusing. We needed a way to pull that data back into our own ecosystem, brand it, and present it clearly.
Architecting the Tracking Widget
Building a tracking widget for Shopify comes with a few architectural hurdles. The biggest challenge is installation friction. If an app requires merchants to manually edit their theme.liquid file, support tickets about broken themes will eat you alive.
I decided to use Shopify’s App Embed blocks. This allows merchants to inject the widget into their order status pages via the theme editor, keeping the theme code completely clean.
Here is the core Liquid and JavaScript snippet I used for the App Embed block. It passes the necessary order data from Shopify's Liquid context into the frontend widget.
{% 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>
The Liquid variables {{ order.id }} and {{ order.name }} are crucial here. They allow the widget to fetch the exact tracking data for that specific order without exposing sensitive customer PII (Personally Identifiable Information) to the client-side script.
On the backend, keeping the tracking data fresh is another challenge. Initially, I thought about having the frontend widget poll the carrier APIs directly. That was a mistake. Carrier APIs have strict rate limits, and exposing API keys to the frontend is a security risk.
Instead, I relied on Shopify Webhooks. Whenever an order is fulfilled or updated, Shopify sends a webhook to our backend. We verify the HMAC signature to ensure it's genuinely from Shopify, then parse the fulfillment data.
Here is the Node.js/Express webhook handler I wrote to process these updates securely:
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');
}
);
By handling the heavy lifting on the backend via webhooks, the frontend widget only needs to make a simple, authenticated request to our own API to get the tracking status. This keeps the widget fast and keeps our carrier API usage well within rate limits.
Practical Results and Lessons Learned
Once the architecture was solid, the results were immediate. The App Embed approach meant zero theme conflicts. Merchants could toggle the widget on and off from the Shopify admin without touching a single line of code.
What surprised me most was the impact of the branded experience. By keeping the customer on the store's domain and mapping the store's CSS variables to the widget, the post-purchase experience felt like a natural extension of the brand.
For example, stores that previously handled dozens of WISMO emails a week saw that number drop significantly. Customers could just click the link in their shipping confirmation, land on a branded page, and see exactly where their package was.
If I were to start over, I would probably spend more time initially on the CSS customization engine. Making a widget truly match every possible Shopify theme's typography and spacing takes a lot of iterative tweaking. I also learned to be very careful with webhook retries; Shopify will retry failed webhooks, so your backend needs to be idempotent to avoid duplicate database entries. Using an upsert operation in the database, as shown in the code above, was a lifesaver for this.
Wrapping Up
I originally built this to solve a specific problem for a friend's store, but I realized many other Shopify merchants were dealing with the exact same WISMO headaches. I packaged the core functionality into a free, easy-to-install tool.
If you're running a Shopify store and want to give your customers a better post-purchase experience without the bloat of heavy enterprise apps, you can check out the free Shipment Tracking Widget at parcelglance.com. It takes about two minutes to install via App Embeds.
Full disclosure: I'm the developer of Parcelglance. I built this because I needed it myself.
Top comments (0)