When I first started helping small Shopify store owners optimize their tech stack, one issue kept coming up. It wasn't about getting more traffic or increasing conversion rates. It was the endless stream of "Where is my order?" emails. Store owners were spending hours every week manually checking carrier websites and replying to anxious customers. I realized the post-purchase experience was completely broken. So, I decided to build something to fix it.
The post-purchase phase is often an afterthought. You spend months perfecting your storefront, checkout flow, and marketing, but once the customer clicks "Buy," they are thrown into a black hole. Shopify's default order status page is functional, but it's generic and doesn't provide real-time, carrier-specific tracking updates in a branded way.
The cost of ignoring this? WISMO (Where Is My Order) tickets. These tickets eat up support time, frustrate customers, and can actually increase refund requests. If a customer feels out of the loop, they assume the worst. We needed a way to keep them engaged and informed without adding manual work for the store owner.
When tackling post-purchase tracking, developers generally have three approaches:
-
The Native Liquid Approach: Modifying the
order-status.liquidtemplate directly.- Pros: No external dependencies, fast.
- Cons: Hard to maintain, breaks on theme updates, limited to Shopify's native tracking data.
-
The Comprehensive SaaS Integration: Installing a large, all-in-one tracking app.
- Pros: Feature-rich, handles everything from email notifications to returns.
- Cons: Can slow down the storefront, expensive, and often overkill if you just need a simple tracking widget.
-
The Lightweight App Embed Block: Using Shopify's Theme App Extensions to inject a custom, lightweight tracking widget.
- Pros: Keeps the theme clean, easy to install, highly customizable, doesn't bloat the core theme files.
- Cons: Requires writing custom JavaScript and handling API integrations yourself.
I chose the third approach. It offers a great balance of performance and flexibility. Let's look at how the frontend widget is actually embedded using Shopify's App Embed blocks.
{% 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 snippet injects a container div and asynchronously loads the tracking script. By passing the orderId, orderNumber, and storeName via data attributes, the widget can fetch the exact tracking details for that specific order without needing a complex page reload. The async loading ensures we don't block the main thread or delay the rendering of the rest of the order status page.
But what about the backend? To get real-time tracking, we need to listen to Shopify's webhooks. When an order is fulfilled, Shopify sends a payload. We need to verify it and store the tracking data 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');
}
);
Security is paramount here. We verify the HMAC signature to ensure the webhook actually came from Shopify. Once verified, we extract the tracking number and carrier, storing it in our database so the frontend widget can query it later. This decoupled architecture means the frontend remains lightweight, while the backend handles the heavy lifting of carrier API integrations.
Building this lightweight approach yielded some interesting insights. What worked really well was the App Embed block. Store owners loved that they could toggle the tracking widget on or off directly from the Shopify theme editor without touching a single line of code. It made the installation process frictionless and allowed them to customize the widget to match their store theme easily.
What surprised me was the impact on support tickets. By just providing a clean, branded, and up-to-date tracking page, the volume of WISMO emails dropped significantly. Customers just wanted visibility; they didn't necessarily need a human to tell them their package was in transit. The post-purchase engagement kept them informed and reduced their anxiety.
If I were to change anything, I would probably invest more time early on in building a more robust fallback for when carrier APIs experience downtime. Relying solely on third-party carrier APIs means you're at the mercy of their uptime, so having a graceful degradation state in the UI is crucial for a smooth customer experience.
If you're a Shopify developer or store owner looking to improve your post-purchase experience without the bloat of heavy apps, I've packaged this approach into a free tool. You can check out the Shipment Tracking Widget at parcelglance.com to see how a lightweight, branded tracking page can transform your customer experience.
Full disclosure: I'm the developer of Parcelglance. I built this because I needed it myself.
Top comments (0)