When I first started helping a few friends manage their Shopify stores, I quickly noticed a frustrating pattern. The moment a customer clicked "purchase," the store owner's job wasn't done; it actually just began. The support inbox would slowly fill up with the dreaded WISMO emails: "Where is my order?"
Customers were getting anxious because the post-purchase experience was just a generic, unbranded redirect to a carrier's website. They had no idea what was happening with their package, and the store owners were spending hours manually checking tracking numbers to reply to emails.
I looked at the existing solutions, but they either felt clunky, required heavy theme modifications, or locked you into expensive monthly tiers for basic features. So I decided to build something to fix the post-purchase experience.
The Problem: Evaluating Tracking Approaches
Before writing any code, I had to step back and analyze the different ways to solve the WISMO problem. If you're building a post-purchase experience, you generally have three architectural approaches:
1. The Carrier Redirect Approach
This is the default for many basic setups. When a customer clicks "track order," they are sent to a third-party site like 17track or the carrier's native portal.
- Pros: Zero development effort. It works out of the box.
- Cons: Terrible user experience. It breaks the brand immersion, often leads to confusing UI for the customer, and increases the bounce rate away from your store.
2. The Custom Portal Approach
You build a dedicated /track page on your store and write custom integrations for every carrier API (UPS, FedEx, DHL, etc.) to fetch and display data.
- Pros: Complete control over the UI and data flow. Great branding.
- Cons: Massive technical debt. Maintaining integrations with dozens of carrier APIs, handling their rate limits, and parsing their inconsistent webhook formats is a full-time job.
3. The Embedded Widget Approach
Instead of a separate page, you inject a tracking widget directly into the customer's order status page or account dashboard using Shopify's native theme architecture.
- Pros: Keeps the customer on your store, provides a seamless branded experience, and is relatively easy to maintain if abstracted correctly.
- Cons: Requires a solid understanding of Shopify's Liquid templating and the newer App Embed block architecture.
I chose the Embedded Widget approach. It offered the best balance of user experience and technical feasibility. But implementing it on Shopify required some careful architectural decisions.
Technical Approach: App Embeds and Webhooks
The biggest technical hurdle was figuring out how to render the widget without forcing merchants to manually edit their theme code. In the past, you'd have to inject Liquid snippets directly into the order-status.php or theme files, which was a nightmare for updates.
Shopify's App Embed blocks changed this. They allow apps to inject UI components into supported theme sections natively.
Here is the core Liquid and JavaScript I used to render the widget on the order status page:
{% 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>
By passing the order.id and order.name via data attributes, the frontend script can securely request the specific tracking data for that order from our backend without exposing sensitive customer info in the URL.
However, rendering the widget is only half the battle. We also need the actual tracking data. Initially, I considered polling the carrier APIs every time the widget loaded, but that was inefficient and prone to rate-limiting.
Instead, I opted for an event-driven architecture using Shopify Webhooks. When an order is fulfilled, Shopify sends a webhook to our server. We verify the HMAC signature to ensure it's genuinely from Shopify, then extract the tracking details.
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');
}
);
This approach means we only fetch real-time tracking updates from the carrier when the widget actually requests it, using the tracking number we securely stored via the webhook. It keeps the widget lightweight and the backend responsive.
Practical Results and Trade-offs
Building this taught me a lot about the Shopify ecosystem. What worked exceptionally well was the App Embed architecture. Merchants can install the widget via the theme editor without touching a single line of code. It drastically reduced the friction of onboarding.
What surprised me was the sheer inconsistency of carrier APIs. Even when abstracting them behind our backend, parsing the varying JSON structures from different logistics providers required a lot of defensive coding.
If I were to start over, I would implement a stricter TypeScript setup for the widget initialization from day one. The frontend script evolved quickly, and retrofitting types later was more painful than it needed to be. I'd also lean heavier into caching carrier responses at the edge to reduce latency for the end customer.
Wrapping Up
Reducing WISMO tickets isn't just about saving support time; it's about building trust. When customers have a clear, branded view of their order's journey, they feel confident in their purchase.
If you're a Shopify merchant dealing with post-purchase anxiety, or a developer looking to see how App Embeds work in practice, I've packaged this solution into a free tool. You can check out the Shipment Tracking Widget at parcelglance.com to see how it integrates with your store's theme.
Full disclosure: I'm the developer of Parcelglance. I built this because I needed it myself.
Top comments (0)