I remember staring at my inbox at 11 PM on a Tuesday, completely overwhelmed. I was running a small Shopify store, and while the sales were great, the post-purchase experience was a nightmare. My inbox was flooded with the same question: "Where is my order?"
I was spending hours manually checking carrier websites, copying tracking numbers, and replying to customers. It was draining my time, killing my productivity, and frankly, frustrating the people who had just trusted me with their money. The post-purchase phase, which should have been a moment of excitement for my customers, had turned into a customer support black hole.
So I decided to build something.
The Post-Purchase Black Hole
When a customer clicks "pay" on your store, the transaction is technically complete. But psychologically, their journey is just beginning. They are now in a state of anticipation.
If you don't give them visibility into their order, that anticipation quickly turns into anxiety. The cost of ignoring this phase is high. Support ticket volumes spike, customer trust erodes, and repeat purchase rates suffer. "Where Is My Order" (WISMO) tickets are notoriously the most common support requests in e-commerce. They are also the most avoidable, yet many stores still rely on generic, unbranded confirmation emails and leave customers to figure it out themselves.
I realized that keeping customers informed shouldn't require me to manually check tracking sites. It needed to be automated, branded, and seamlessly integrated into the store experience.
The Technical Approach
Building a tracking solution for Shopify comes with specific architectural challenges. You can't just inject scripts anywhere; Shopify's ecosystem is strict about security and theme modifications.
My first major decision was to use Shopify App Embed blocks. In the past, apps required merchants to manually edit their theme.liquid files. This was a nightmare for store owners and often broke when themes were updated. App Embed blocks allow merchants to toggle features on and off via the theme editor, and inject the code safely without touching the core theme files.
The second challenge was data synchronization. I needed to know exactly when an order was fulfilled so I could start fetching tracking data from the carriers. This meant setting up a secure webhook listener on my backend.
Here is how I handled the fulfillment webhook. Security is paramount here, so verifying the Shopify HMAC signature is non-negotiable:
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 parsing the raw body and verifying the HMAC, I ensure that only legitimate Shopify events trigger my tracking logic. Once the fulfillment data is stored, a background job can safely poll the carrier APIs without overwhelming them.
On the frontend, I needed to render the widget on the order status page. Using the App Embed block, I pass the necessary Liquid variables into the DOM, and then initialize a lightweight vanilla JavaScript widget. I chose vanilla JS over a heavy framework like React to keep the widget's footprint small and ensure it doesn't slow down the merchant's checkout or order status pages.
Here is the Liquid and JavaScript snippet that powers the embed:
{% 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>
Passing the order data via data-* attributes keeps the initialization clean and avoids inline script execution issues, which is crucial for adhering to strict Content Security Policies (CSP).
Practical Results and Lessons Learned
The App Embed approach completely changed the installation experience. What used to take merchants 20 minutes of digging through theme code now takes a few clicks in the theme editor.
One thing that surprised me was the impact of branding. When customers are bounced to a carrier's generic website, they feel disconnected from the store. By keeping the tracking experience branded and on-domain, the post-purchase experience feels like a natural continuation of the store, rather than an afterthought.
If I could go back and change one thing, it would be my initial approach to carrier API polling. Early on, I polled the carrier APIs too frequently for every single order. I quickly hit rate limits and had to rethink my architecture. I ended up implementing a smarter caching strategy and exponential backoff for tracking updates, which drastically reduced server load and kept the data accurate without violating carrier terms.
Wrapping Up
Building this tool solved my immediate problem, but I realized many other indie merchants were struggling with the exact same WISMO headaches. I packaged up the solution, refined the UI, and made it easy to install.
If you're dealing with post-purchase support tickets and want to give your customers a branded, real-time tracking experience without touching your theme code, I'd love for you to try it out.
You can grab the free Shipment Tracking Widget at parcelglance.com. It's built to be lightweight, easy to install, and focused entirely on making the post-purchase experience better for your customers.
Full disclosure: I'm the developer of Parcelglance. I built this because I needed it myself.
Top comments (0)