Learn how to easily add order tracking to your web app and enhance customer experience with our step-by-step guide.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Why Order Tracking Matters
Let's be honest—customers who can track their orders are happier customers. According to a 2022 Shopify report, 83% of consumers expect regular updates on their purchases, and 53% won't purchase again from companies that leave them in the dark. Adding order tracking isn't just a nice-to-have feature; it's practically table stakes for modern ecommerce.
Think of your order tracking database like the foundation of a house—if it's weak, everything built on top will be unstable. Here's what you'll need:
-- Core orders table
CREATE TABLE orders (
id VARCHAR(36) PRIMARY KEY,
customer_id VARCHAR(36) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
total_amount DECIMAL(10, 2) NOT NULL,
shipping_address_id VARCHAR(36) NOT NULL,
expected_delivery_date DATE,
current_status VARCHAR(50) DEFAULT 'pending',
tracking_number VARCHAR(100),
shipping_carrier VARCHAR(100),
FOREIGN KEY (customer_id) REFERENCES customers(id),
FOREIGN KEY (shipping_address_id) REFERENCES addresses(id)
);
-- Order status history for tracking every state change
CREATE TABLE order_status_history (
id VARCHAR(36) PRIMARY KEY,
order_id VARCHAR(36) NOT NULL,
status VARCHAR(50) NOT NULL,
location VARCHAR(255),
notes TEXT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_by VARCHAR(100),
FOREIGN KEY (order_id) REFERENCES orders(id)
);
-- Order items for detailed view
CREATE TABLE order_items (
id VARCHAR(36) PRIMARY KEY,
order_id VARCHAR(36) NOT NULL,
product_id VARCHAR(36) NOT NULL,
quantity INT NOT NULL,
price DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
Key design decisions:
You'll need at least these four endpoints to power a comprehensive tracking system:
// Express.js implementation example
const express = require('express');
const router = express.Router();
const { authenticateUser, authenticateAdmin } = require('../middleware/auth');
const OrderService = require('../services/OrderService');
// 1. Get order details with current status (customer-facing)
router.get('/orders/:orderId', authenticateUser, async (req, res) => {
try {
const { orderId } = req.params;
const { userId } = req.user;
const order = await OrderService.getOrderDetails(orderId, userId);
if (!order) {
return res.status(404).json({ message: 'Order not found' });
}
res.json(order);
} catch (error) {
console.error('Error fetching order:', error);
res.status(500).json({ message: 'Failed to retrieve order details' });
}
});
// 2. Get complete order status history (customer-facing)
router.get('/orders/:orderId/history', authenticateUser, async (req, res) => {
try {
const { orderId } = req.params;
const { userId } = req.user;
const history = await OrderService.getOrderStatusHistory(orderId, userId);
res.json(history);
} catch (error) {
console.error('Error fetching order history:', error);
res.status(500).json({ message: 'Failed to retrieve order history' });
}
});
// 3. Update order status (admin-facing)
router.post('/admin/orders/:orderId/status', authenticateAdmin, async (req, res) => {
try {
const { orderId } = req.params;
const { status, location, notes } = req.body;
const { userId } = req.user;
const updatedOrder = await OrderService.updateOrderStatus(
orderId,
status,
{
location,
notes,
updatedBy: userId
}
);
// Trigger notifications after successful update
await NotificationService.notifyOrderStatusChange(updatedOrder);
res.json(updatedOrder);
} catch (error) {
console.error('Error updating order status:', error);
res.status(500).json({ message: 'Failed to update order status' });
}
});
// 4. Bulk import tracking numbers (admin-facing)
router.post('/admin/orders/tracking-import', authenticateAdmin, async (req, res) => {
try {
const { trackingData } = req.body;
// trackingData format: [{orderId, trackingNumber, carrier}]
const results = await OrderService.bulkUpdateTracking(trackingData);
res.json({
success: true,
updated: results.length,
results
});
} catch (error) {
console.error('Error importing tracking data:', error);
res.status(500).json({ message: 'Failed to import tracking data' });
}
});
module.exports = router;
Security considerations:
You have three main approaches for keeping order status information current:
// Order service implementation
class OrderService {
// ... other methods
static async updateOrderStatus(orderId, status, details = {}) {
const { location, notes, updatedBy } = details;
// Start a database transaction
const transaction = await db.beginTransaction();
try {
// Update the current status in the orders table
await db.query(
'UPDATE orders SET current_status = ? WHERE id = ?',
[status, orderId],
transaction
);
// Add an entry to the status history table
await db.query(
`INSERT INTO order_status_history
(id, order_id, status, location, notes, updated_by)
VALUES (?, ?, ?, ?, ?, ?)`,
[uuidv4(), orderId, status, location, notes, updatedBy],
transaction
);
// Commit the transaction
await transaction.commit();
// Return the updated order
return this.getOrderDetails(orderId);
} catch (error) {
await transaction.rollback();
throw error;
}
}
}
// Carrier tracking integration service
class CarrierTrackingService {
static carriers = {
'ups': {
baseUrl: 'https://api.ups.com/api/tracking/v1',
apiKey: process.env.UPS_API_KEY,
fetchTrackingInfo: async function(trackingNumber) {
// Implementation for UPS API
const response = await fetch(`${this.baseUrl}/details/${trackingNumber}`, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`UPS API error: ${response.status}`);
}
const data = await response.json();
return this.normalizeTrackingData(data);
},
normalizeTrackingData: function(data) {
// Transform carrier-specific response to our standardized format
return {
status: this.mapStatus(data.trackResponse.shipment[0].package[0].activity[0].status.description),
location: data.trackResponse.shipment[0].package[0].activity[0].location.address.city,
timestamp: new Date(data.trackResponse.shipment[0].package[0].activity[0].date + ' ' +
data.trackResponse.shipment[0].package[0].activity[0].time),
estimatedDelivery: data.trackResponse.shipment[0].package[0].deliveryDate ?
new Date(data.trackResponse.shipment[0].package[0].deliveryDate[0].date) : null,
events: data.trackResponse.shipment[0].package[0].activity.map(act => ({
status: this.mapStatus(act.status.description),
location: act.location.address.city,
timestamp: new Date(act.date + ' ' + act.time),
description: act.status.description
}))
};
},
mapStatus: function(carrierStatus) {
// Map carrier-specific statuses to our standardized statuses
const statusMap = {
'In Transit': 'in_transit',
'Delivered': 'delivered',
'Exception': 'exception',
'Out for Delivery': 'out_for_delivery'
// ... more mappings
};
return statusMap[carrierStatus] || 'unknown';
}
},
'fedex': {
// Similar implementation for FedEx
},
'usps': {
// Similar implementation for USPS
}
};
static async getTrackingInfo(trackingNumber, carrier) {
if (!this.carriers[carrier.toLowerCase()]) {
throw new Error(`Unsupported carrier: ${carrier}`);
}
return this.carriers[carrier.toLowerCase()].fetchTrackingInfo(trackingNumber);
}
static async syncOrderTracking(orderId) {
// Get order details
const order = await OrderService.getOrderDetails(orderId);
if (!order.tracking_number || !order.shipping_carrier) {
throw new Error('Order does not have tracking information');
}
// Fetch latest tracking info from carrier
const trackingInfo = await this.getTrackingInfo(
order.tracking_number,
order.shipping_carrier
);
// Update order status with all tracking events
for (const event of trackingInfo.events) {
await OrderService.updateOrderStatus(
orderId,
event.status,
{
location: event.location,
notes: event.description,
updatedBy: 'carrier_api',
timestamp: event.timestamp
}
);
}
// Update estimated delivery date if available
if (trackingInfo.estimatedDelivery) {
await OrderService.updateEstimatedDelivery(
orderId,
trackingInfo.estimatedDelivery
);
}
return trackingInfo;
}
}
// Using a job queue like Bull with Redis
const Queue = require('bull');
const trackingQueue = new Queue('order-tracking-updates', {
redis: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
}
});
// Producer: Schedule tracking updates for orders
async function scheduleTrackingUpdates() {
// Find orders that need updates (not delivered and have tracking info)
const orders = await db.query(`
SELECT id, tracking_number, shipping_carrier
FROM orders
WHERE current_status != 'delivered'
AND tracking_number IS NOT NULL
`);
for (const order of orders) {
// Add job to queue with order details
await trackingQueue.add(
{
orderId: order.id,
trackingNumber: order.tracking_number,
carrier: order.shipping_carrier
},
{
attempts: 3,
backoff: {
type: 'exponential',
delay: 60000 // 1 minute
}
}
);
}
}
// Consumer: Process tracking update jobs
trackingQueue.process(async (job) => {
const { orderId, trackingNumber, carrier } = job.data;
try {
// Use the carrier integration service to sync tracking
await CarrierTrackingService.syncOrderTracking(orderId);
return { success: true, orderId };
} catch (error) {
console.error(`Tracking sync failed for order ${orderId}:`, error);
throw error; // This will trigger retry based on the backoff config
}
});
// Schedule job to run every hour
const CronJob = require('cron').CronJob;
new CronJob('0 * * * *', scheduleTrackingUpdates, null, true);
Implementation advice:
For most small to medium businesses, I recommend starting with Option 2 (carrier API integration) for automatic updates and supplementing with Option 1 (manual updates) for edge cases. The job queue approach (Option 3) becomes necessary when you're processing more than a few hundred orders per day.
Your tracking interface needs to be clear, reassuring, and accessible. Here's a React implementation example:
// OrderTrackingPage.jsx
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import axios from 'axios';
import { format } from 'date-fns';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
// Status icons and colors for visual feedback
const statusConfig = {
'pending': { icon: 'clock', color: '#f0ad4e', label: 'Order Pending' },
'processing': { icon: 'cogs', color: '#5bc0de', label: 'Processing' },
'shipped': { icon: 'truck', color: '#5cb85c', label: 'Shipped' },
'in_transit': { icon: 'shipping-fast', color: '#0275d8', label: 'In Transit' },
'out_for_delivery': { icon: 'truck-loading', color: '#6f42c1', label: 'Out for Delivery' },
'delivered': { icon: 'check-circle', color: '#28a745', label: 'Delivered' },
'exception': { icon: 'exclamation-triangle', color: '#dc3545', label: 'Delivery Exception' }
};
function OrderTrackingPage() {
const { orderId } = useParams();
const [order, setOrder] = useState(null);
const [statusHistory, setStatusHistory] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Fetch order details and history
useEffect(() => {
const fetchOrderData = async () => {
try {
setLoading(true);
// Fetch basic order details
const orderResponse = await axios.get(`/api/orders/${orderId}`);
setOrder(orderResponse.data);
// Fetch complete status history
const historyResponse = await axios.get(`/api/orders/${orderId}/history`);
setStatusHistory(historyResponse.data);
setLoading(false);
} catch (err) {
setError('Unable to load tracking information. Please try again later.');
setLoading(false);
console.error('Error fetching order tracking:', err);
}
};
fetchOrderData();
}, [orderId]);
if (loading) return <div className="loading-spinner">Loading tracking information...</div>;
if (error) return <div className="error-message">{error}</div>;
if (!order) return <div className="not-found">Order not found</div>;
// Get the latest location for map display
const latestLocation = statusHistory.length > 0 ? statusHistory[0].location : null;
// Mock coordinates based on location name (in a real app, you'd use geocoding)
const getCoordinates = (location) => {
// This would be replaced with actual geocoding
const mockCoordinates = {
'New York': [40.7128, -74.0060],
'Los Angeles': [34.0522, -118.2437],
'Chicago': [41.8781, -87.6298],
// More locations...
};
return mockCoordinates[location] || [39.8283, -98.5795]; // Default to center of US
};
const currentStatus = order.current_status;
const { icon, color, label } = statusConfig[currentStatus] || statusConfig.pending;
return (
<div className="order-tracking-container">
<div className="order-header">
<h2>Track Order #{order.id.substring(0, 8)}</h2>
<div className="estimated-delivery">
{order.expected_delivery_date ? (
<>
<span className="label">Estimated Delivery:</span>
<span className="date">{format(new Date(order.expected_delivery_date), 'MMMM d, yyyy')}</span>
</>
) : (
<span>Delivery estimate pending</span>
)}
</div>
</div>
{/* Current Status Badge */}
<div className="current-status" style={{ backgroundColor: color }}>
<i className={`fas fa-${icon}`}></i>
<span>{label}</span>
</div>
{/* Progress Tracker */}
<div className="progress-tracker">
{Object.keys(statusConfig).map((status, index) => {
const isCompleted = determineIfCompleted(status, currentStatus);
const isCurrent = status === currentStatus;
return (
<div
key={status}
className={`progress-step ${isCompleted ? 'completed' : ''} ${isCurrent ? 'current' : ''}`}
>
<div className="step-icon" style={{ backgroundColor: isCompleted || isCurrent ? statusConfig[status].color : '#ccc' }}>
<i className={`fas fa-${statusConfig[status].icon}`}></i>
</div>
<div className="step-label">{statusConfig[status].label}</div>
{index < Object.keys(statusConfig).length - 1 && (
<div className={`connector-line ${isCompleted ? 'completed' : ''}`}></div>
)}
</div>
);
})}
</div>
{/* Tracking details and map */}
<div className="tracking-details-container">
<div className="tracking-history">
<h3>Tracking History</h3>
<div className="timeline">
{statusHistory.map((event, index) => (
<div key={index} className="timeline-event">
<div className="event-date">
{format(new Date(event.timestamp), 'MMM d, yyyy - h:mm a')}
</div>
<div className="event-dot" style={{ backgroundColor: statusConfig[event.status]?.color || '#ccc' }}></div>
<div className="event-details">
<div className="event-status">{statusConfig[event.status]?.label || event.status}</div>
{event.location && <div className="event-location">{event.location}</div>}
{event.notes && <div className="event-notes">{event.notes}</div>}
</div>
</div>
))}
</div>
</div>
{/* Map visualization if we have location data */}
{latestLocation && (
<div className="tracking-map">
<h3>Current Location</h3>
<MapContainer
center={getCoordinates(latestLocation)}
zoom={5}
style={{ height: '300px', width: '100%' }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
<Marker position={getCoordinates(latestLocation)}>
<Popup>
<b>Current Status:</b> {statusConfig[currentStatus]?.label}<br />
<b>Location:</b> {latestLocation}
</Popup>
</Marker>
</MapContainer>
</div>
)}
</div>
{/* Order details summary */}
<div className="order-summary">
<h3>Order Summary</h3>
<div className="order-details">
<div className="detail-row">
<span className="detail-label">Order Date:</span>
<span className="detail-value">{format(new Date(order.created_at), 'MMMM d, yyyy')}</span>
</div>
{order.tracking_number && (
<div className="detail-row">
<span className="detail-label">Tracking Number:</span>
<span className="detail-value">{order.tracking_number}</span>
</div>
)}
{order.shipping_carrier && (
<div className="detail-row">
<span className="detail-label">Carrier:</span>
<span className="detail-value">{order.shipping_carrier}</span>
</div>
)}
<div className="detail-row">
<span className="detail-label">Items:</span>
<span className="detail-value">{order.items?.length || 0}</span>
</div>
<div className="detail-row">
<span className="detail-label">Total:</span>
<span className="detail-value">${order.total_amount}</span>
</div>
</div>
</div>
</div>
);
}
// Helper function to determine if a status step is completed
function determineIfCompleted(stepStatus, currentStatus) {
const statusOrder = [
'pending', 'processing', 'shipped', 'in_transit',
'out_for_delivery', 'delivered', 'exception'
];
const stepIndex = statusOrder.indexOf(stepStatus);
const currentIndex = statusOrder.indexOf(currentStatus);
// Special handling for exception status
if (currentStatus === 'exception') {
return stepIndex < statusOrder.indexOf('exception');
}
return stepIndex <= currentIndex;
}
export default OrderTrackingPage;
UX considerations:
Don't make customers hunt for updates—push them automatically:
// NotificationService.js
class NotificationService {
static async notifyOrderStatusChange(order) {
const customer = await CustomerService.getCustomerById(order.customer_id);
const statusDetails = this.getStatusDetails(order.current_status);
// Determine which channels to use based on customer preferences
const channels = customer.notification_preferences || ['email'];
// Build the notification content
const content = {
title: `Order #${order.id.substring(0, 8)} ${statusDetails.titleText}`,
body: this.buildNotificationBody(order, statusDetails),
actionUrl: `${process.env.FRONTEND_URL}/orders/${order.id}/tracking`,
imageUrl: statusDetails.imageUrl
};
// Send notifications through each channel
const notifications = [];
for (const channel of channels) {
try {
switch (channel) {
case 'email':
notifications.push(await this.sendEmail(customer.email, content));
break;
case 'sms':
if (customer.phone) {
notifications.push(await this.sendSms(customer.phone, content));
}
break;
case 'push':
if (customer.push_tokens && customer.push_tokens.length > 0) {
notifications.push(await this.sendPushNotification(customer.push_tokens, content));
}
break;
}
} catch (error) {
console.error(`Failed to send ${channel} notification:`, error);
}
}
// Log all notification attempts
await this.logNotificationAttempts(order.id, notifications);
return notifications;
}
static getStatusDetails(status) {
const statusDetails = {
'pending': {
titleText: 'has been received',
messageText: 'We\'ve received your order and are preparing to process it.',
imageUrl: '/images/notifications/order-received.png'
},
'processing': {
titleText: 'is being processed',
messageText: 'Your order is now being processed and prepared for shipping.',
imageUrl: '/images/notifications/order-processing.png'
},
'shipped': {
titleText: 'has shipped',
messageText: 'Great news! Your order is on its way to you.',
imageUrl: '/images/notifications/order-shipped.png'
},
'in_transit': {
titleText: 'is in transit',
messageText: 'Your package is on the move and making its way to you.',
imageUrl: '/images/notifications/order-in-transit.png'
},
'out_for_delivery': {
titleText: 'is out for delivery',
messageText: 'Your package is out for delivery today!',
imageUrl: '/images/notifications/order-out-for-delivery.png'
},
'delivered': {
titleText: 'has been delivered',
messageText: 'Your order has been delivered! Thank you for shopping with us.',
imageUrl: '/images/notifications/order-delivered.png'
},
'exception': {
titleText: 'has a delivery exception',
messageText: 'There\'s an issue with your delivery. Please check the tracking page for details.',
imageUrl: '/images/notifications/order-exception.png'
}
};
return statusDetails[status] || {
titleText: 'has been updated',
messageText: 'Your order status has been updated.',
imageUrl: '/images/notifications/order-updated.png'
};
}
static buildNotificationBody(order, statusDetails) {
let body = statusDetails.messageText;
// Add tracking info if available
if (order.tracking_number && order.shipping_carrier) {
body += ` Track your package with ${order.shipping_carrier} using tracking number ${order.tracking_number}.`;
}
// Add delivery estimate if available
if (order.expected_delivery_date) {
const formattedDate = new Date(order.expected_delivery_date).toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric'
});
body += ` Expected delivery: ${formattedDate}.`;
}
return body;
}
static async sendEmail(email, content) {
// Implementation using your email service provider
// Example with SendGrid:
const sgMail = require('@sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const msg = {
to: email,
from: process.env.NOTIFICATION_EMAIL,
subject: content.title,
text: content.body,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<img src="${process.env.FRONTEND_URL}${content.imageUrl}" alt="Order Status" style="max-width: 100%; height: auto;" />
<h2>${content.title}</h2>
<p>${content.body}</p>
<p><a href="${content.actionUrl}" style="display: inline-block; background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px;">Track Your Order</a></p>
</div>
`
};
const response = await sgMail.send(msg);
return {
channel: 'email',
recipient: email,
status: response[0].statusCode === 202 ? 'sent' : 'failed',
sentAt: new Date()
};
}
static async sendSms(phone, content) {
// Implementation using your SMS service provider
// Example with Twilio:
const twilio = require('twilio');
const client = twilio(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN
);
// Keep SMS content concise
const smsText = `${content.title}. ${content.body.substring(0, 100)}... Track at: ${content.actionUrl}`;
const message = await client.messages.create({
body: smsText,
from: process.env.TWILIO_PHONE_NUMBER,
to: phone
});
return {
channel: 'sms',
recipient: phone,
status: message.status,
sentAt: new Date()
};
}
static async sendPushNotification(tokens, content) {
// Implementation using Firebase Cloud Messaging or similar
// Example with FCM:
const admin = require('firebase-admin');
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert(JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT))
});
}
const message = {
notification: {
title: content.title,
body: content.body.substring(0, 150) // Keep push notifications concise
},
data: {
url: content.actionUrl,
orderId: content.orderId
},
tokens: tokens
};
const response = await admin.messaging().sendMulticast(message);
return {
channel: 'push',
recipient: tokens.join(','),
status: response.successCount > 0 ? 'sent' : 'failed',
sentAt: new Date()
};
}
static async logNotificationAttempts(orderId, notifications) {
// Store notification history in database
for (const notification of notifications) {
await db.query(`
INSERT INTO notification_log
(id, order_id, channel, recipient, status, sent_at)
VALUES (?, ?, ?, ?, ?, ?)
`, [
uuidv4(),
orderId,
notification.channel,
notification.recipient,
notification.status,
notification.sentAt
]);
}
}
}
module.exports = NotificationService;
Communication best practices:
Once you've built your tracking system, keep these factors in mind for a smooth deployment:
Once you have the basics working, consider these advanced features:
For a typical mid-sized web application, here's a realistic implementation schedule:
Building a robust order tracking system isn't just about technical implementation—it's about creating transparency and trust with your customers. Every status update is an opportunity to reinforce your brand and reduce support tickets.
With the architecture outlined above, you'll have a flexible system that can evolve with your business needs while maintaining the performance and reliability your customers expect.
Remember that for many customers, order tracking isn't a feature—it's an expectation. Meeting and exceeding those expectations turns anxious waiting into a positive extension of your customer experience.
Explore the top 3 order tracking use cases to enhance your web app’s customer experience and efficiency.
A real-time view into order status that reduces customer anxiety and support inquiries. By providing transparent tracking milestones from payment confirmation through delivery, customers gain confidence in your fulfillment process, reducing the "where is my order?" support burden by up to 60%.
A data collection mechanism that captures fulfillment performance metrics at each processing stage. This transforms order tracking from a customer-facing feature into an internal optimization tool that identifies bottlenecks and inefficiencies in your operations.
A communication channel that creates natural engagement moments throughout the fulfillment journey. Each tracking update becomes an opportunity to deepen the customer relationship through contextual messaging, complementary product recommendations, or satisfaction surveys.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â