Shipping notifications reduce support tickets and build customer confidence. Here's how to design effective shipping emails throughout the delivery journey.
Shipping email sequence
Order shipped
interface ShippingNotification {
order: {
id: string;
items: OrderItem[];
};
shipping: {
carrier: string;
trackingNumber: string;
trackingUrl: string;
estimatedDelivery: Date;
shippedAt: Date;
};
customer: {
name: string;
email: string;
};
address: ShippingAddress;
}
await sendEmail({
to: customer.email,
subject: `Your order has shipped! ๐ฆ`,
template: 'order-shipped',
data: {
customer,
order,
shipping,
trackingUrl: shipping.trackingUrl,
estimatedDelivery: formatDate(shipping.estimatedDelivery)
}
});
Out for delivery
await sendEmail({
to: customer.email,
subject: 'Your order is out for delivery! ๐',
template: 'out-for-delivery',
data: {
customer,
order,
estimatedWindow: '2:00 PM - 6:00 PM',
trackingUrl: shipping.trackingUrl
}
});
Delivered confirmation
await sendEmail({
to: customer.email,
subject: 'Your order has been delivered โ',
template: 'order-delivered',
data: {
customer,
order,
deliveredAt: shipping.deliveredAt,
deliveryPhoto: shipping.proofOfDelivery,
reviewUrl: `${baseUrl}/orders/${order.id}/review`,
supportUrl: `${baseUrl}/support`
}
});
Exception handling
Delivery exception
const exceptionTemplates = {
'address_issue': {
subject: 'Delivery issue - address update needed',
template: 'delivery-exception-address'
},
'recipient_unavailable': {
subject: 'Delivery attempted - no one home',
template: 'delivery-exception-unavailable'
},
'weather_delay': {
subject: 'Shipping delay due to weather',
template: 'delivery-exception-weather'
}
};
async function sendExceptionNotification(order: Order, exception: DeliveryException) {
const config = exceptionTemplates[exception.type];
await sendEmail({
to: order.customer.email,
subject: config.subject,
template: config.template,
data: {
order,
exception,
newEstimate: exception.newEstimatedDelivery,
actionRequired: exception.requiresAction,
updateUrl: `${baseUrl}/orders/${order.id}/update-address`
}
});
}
Failed delivery
await sendEmail({
to: customer.email,
subject: 'Delivery unsuccessful - action required',
template: 'delivery-failed',
data: {
customer,
order,
reason: exception.reason,
options: [
{ label: 'Reschedule delivery', url: rescheduleUrl },
{ label: 'Update address', url: updateAddressUrl },
{ label: 'Hold at facility', url: holdUrl }
],
deadline: 'Package will be returned after 5 days'
}
});
Multi-package orders
interface MultiPackageShipment {
order: Order;
packages: Array<{
packageNumber: number;
totalPackages: number;
trackingNumber: string;
items: OrderItem[];
status: string;
}>;
}
await sendEmail({
to: customer.email,
subject: `Package 1 of 3 shipped - Order #${order.number}`,
template: 'multi-package-shipped',
data: {
customer,
order,
package: packages[0],
remainingPackages: packages.slice(1),
allTrackingUrl: `${baseUrl}/orders/${order.id}/tracking`
}
});
Carrier integration
Webhook handling
async function handleCarrierWebhook(event: CarrierEvent) {
const order = await getOrderByTracking(event.trackingNumber);
const eventMap = {
'shipped': sendShippedNotification,
'in_transit': sendTransitUpdate,
'out_for_delivery': sendOutForDeliveryNotification,
'delivered': sendDeliveredNotification,
'exception': sendExceptionNotification
};
const handler = eventMap[event.status];
if (handler) {
await handler(order, event);
}
}
Best practices
- โReal-time updates - Send as soon as status changes
- โClear tracking links - One click to carrier tracking
- โSet expectations - Estimated delivery windows
- โHandle exceptions - Proactive communication on delays
- โInclude order details - Remind them what's coming
- โMobile-friendly - Most check shipping on phones
Good shipping emails reduce "where's my order?" support tickets and build customer confidence.