The sale isn't the finish line. For e-commerce, most of the money is made after checkout. A customer who just paid is paying more attention to your brand than they ever will again. What you send them in the next 14 days determines whether they buy again or forget you exist.
Most stores nail the order confirmation and then go silent until the next marketing blast. That gap is where repeat revenue dies. The fix is a structured post-purchase sequence: six emails, timed to the fulfillment lifecycle, that keep the customer engaged through delivery and beyond.
This is the flow that top e-commerce stores run. Here's every email, when it fires, and the code behind each one.
306%
Higher lifetime value from customers who receive post-purchase sequences vs. order confirmation only
40%
Of e-commerce email revenue comes from automated post-purchase flows
$0.18
Average revenue per post-purchase email - 3x higher than promotional blasts
The 6-email post-purchase flow
Each email maps to a moment in the fulfillment lifecycle. The timing isn't arbitrary - it's based on when customers actually open and click.
| # | Trigger | Timing | Goal | |
|---|---|---|---|---|
| 1 | Order confirmation | checkout.completed | Immediate | Reduce anxiety, set expectations |
| 2 | Shipping notification | fulfillment.shipped | When carrier scans | Build anticipation |
| 3 | Delivery confirmation | fulfillment.delivered | Carrier confirms | Prompt unboxing engagement |
| 4 | Review request | Time-based after delivery | +3 to 5 days | Collect social proof |
| 5 | Refund confirmation | refund.processed | When issued | Retain trust for future purchases |
| 6 | Cross-sell / loyalty | Time-based after delivery | +10 to 14 days | Drive repeat purchase |
Complete post-purchase email sequence with triggers and timing
Email 1: Order confirmation
This is your highest-open-rate email. Order confirmations see 60-70% open rates because customers actively look for them. It's also the email most likely to be forwarded to a spouse, assistant, or accountant, so it doubles as a brand touchpoint you didn't pay for.
What to include
- Order number and date prominently displayed
- Line items with product images, quantities, and prices
- Shipping address and estimated delivery window
- Payment summary with subtotal, tax, shipping, and total
- A clear link to track the order or view order status
import { Section, Row, Column, Text, Img, Hr } from "@react-email/components";
interface OrderItem {
name: string;
image: string;
qty: number;
price: number;
}
interface OrderConfirmationProps {
orderNumber: string;
items: OrderItem[];
subtotal: number;
shipping: number;
tax: number;
total: number;
trackingUrl: string;
}
function LineItem({ item }: { item: OrderItem }) {
return (
<Row style={{ borderBottom: "1px solid #e5e7eb" }}>
<Column style={{ width: "64px", padding: "12px 0" }}>
<Img
src={item.image}
width={48}
height={48}
alt={item.name}
style={{ borderRadius: "6px" }}
/>
</Column>
<Column style={{ padding: "12px 8px" }}>
<Text style={{ margin: 0, fontWeight: 600 }}>{item.name}</Text>
<Text style={{ margin: 0, color: "#6b7280", fontSize: "14px" }}>
Qty: {item.qty}
</Text>
</Column>
<Column style={{ textAlign: "right", padding: "12px 0" }}>
<Text style={{ margin: 0, fontWeight: 600 }}>
${(item.price * item.qty).toFixed(2)}
</Text>
</Column>
</Row>
);
}The typed props mean your checkout webhook can pass data straight to the template without runtime coercion. That matters when you're processing hundreds of orders per hour and a single undefined price field renders a broken email.
Email 2: Shipping notification
This one fires when the carrier scans the package. Give the customer a tracking link and get out of the way. Shipping notifications have the second-highest engagement in the flow because customers obsessively click the tracking link.
Key elements
- Carrier name and tracking number (clickable)
- Estimated delivery date
- Shipping address for confirmation
- Link to order details page
interface ShippingNotificationProps {
customerName: string;
orderNumber: string;
carrier: "usps" | "ups" | "fedex" | "dhl";
trackingNumber: string;
trackingUrl: string;
estimatedDelivery: string;
items: { name: string; qty: number }[];
}
// Carrier logo mapping keeps the template clean
const CARRIER_LOGOS: Record<string, string> = {
usps: "https://yourcdn.com/carriers/usps.png",
ups: "https://yourcdn.com/carriers/ups.png",
fedex: "https://yourcdn.com/carriers/fedex.png",
dhl: "https://yourcdn.com/carriers/dhl.png",
};
// The tracking URL pattern varies by carrier
const CARRIER_TRACKING: Record<string, (id: string) => string> = {
usps: (id) => `https://tools.usps.com/go/TrackConfirmAction?tLabels=${id}`,
ups: (id) => `https://www.ups.com/track?tracknum=${id}`,
fedex: (id) => `https://www.fedex.com/fedextrack/?trknbr=${id}`,
dhl: (id) => `https://www.dhl.com/en/express/tracking.html?AWB=${id}`,
};The carrier union type prevents bad values from reaching the template. If your fulfillment system sends an unsupported carrier, TypeScript catches it before a customer sees a broken tracking link.
Email 3: Delivery confirmation
Most stores skip this one because the carrier already sends a delivery notification. But the carrier email is branded for UPS or FedEx, not for you. Your delivery confirmation is your chance to re-engage before the customer moves on.
The 3-day engagement window
Customers are paying attention in the 3 days after delivery. They're unboxing, trying the product, forming opinions. A delivery email that arrives now - with care instructions, setup tips, or a quick-start guide - lands while they still care. Wait too long and your next touchpoint is a promotional email they ignore.
What to include
- Confirmation that the order was delivered
- Product care or quick-start tips
- Link to your help center or FAQ
- Subtle ask: "Something not right? Let us know."
The "something not right?" link does real work. It gives unhappy customers a direct path to support instead of a one-star review. You hear about problems before everyone else does.
Email 4: Review request
Send the review request too early and the customer hasn't used the product yet. Too late and the excitement is gone. The sweet spot is 3-5 days after delivery for most categories. For products that need a break-in period (shoes, skincare, mattresses), push it to 7-10 days.
interface ReviewRequestProps {
customerName: string;
productName: string;
productImage: string;
reviewUrl: string;
orderNumber: string;
// Optional incentive for leaving a review
incentive?: {
type: "discount" | "points";
value: string; // "10%" or "500 points"
};
}
// The star rating row drives click-through
function StarRating({ reviewUrl }: { reviewUrl: string }) {
return (
<Section style={{ textAlign: "center", padding: "24px 0" }}>
<Text style={{ fontSize: "14px", color: "#6b7280", marginBottom: "8px" }}>
How would you rate your purchase?
</Text>
{/* Each star links to the review page with a pre-selected rating */}
<a href={`${reviewUrl}?rating=5`} style={{ textDecoration: "none" }}>
{"★★★★★"}
</a>
</Section>
);
}Email 5: Refund confirmation
Refund emails feel like a loss, but they're a retention play. A fast, clear refund confirmation builds trust. The customer sees you processed it quickly, knows when it hits their account, and remembers you didn't make it painful. That memory matters the next time they're choosing between you and a competitor.
What to include
- Refund amount and payment method
- Expected processing time (3-5 business days for most processors)
- Original order reference
- Return instructions if the item needs to be shipped back
- Optional: a discount code for their next purchase ("We are sorry it was not the right fit - here is 15% off your next order")
interface RefundConfirmationProps {
customerName: string;
orderNumber: string;
refundAmount: number;
currency: string;
paymentMethod: string; // "Visa ending in 4242"
estimatedArrival: string; // "3-5 business days"
reason?: string;
// Win-back offer
winBackOffer?: {
code: string;
discount: string;
expiresAt: string;
};
}
// Refund breakdown component
function RefundSummary({
refundAmount,
currency,
paymentMethod,
estimatedArrival,
}: Pick<
RefundConfirmationProps,
"refundAmount" | "currency" | "paymentMethod" | "estimatedArrival"
>) {
return (
<Section
style={{
backgroundColor: "#f9fafb",
borderRadius: "8px",
padding: "20px",
margin: "16px 0",
}}
>
<Text style={{ fontSize: "24px", fontWeight: 700, margin: "0 0 4px" }}>
{currency}
{refundAmount.toFixed(2)}
</Text>
<Text style={{ color: "#6b7280", fontSize: "14px", margin: 0 }}>
Refund to {paymentMethod} - {estimatedArrival}
</Text>
</Section>
);
}The optional winBackOfferprop keeps things flexible. Not every refund should include a discount - a product defect calls for a different tone. Making it a prop means it's a business logic decision, not a template change.
Email 6: Cross-sell and loyalty
This one fires 10-14 days after delivery. By now the customer has used the product and formed an opinion. If they're happy (no support tickets, no refund request), it's time to suggest complementary products or introduce a loyalty program.
Two approaches that work
Product-based cross-sell:Recommend 2-3 products related to what they purchased. "You bought running shoes - customers who bought these also picked up moisture-wicking socks and a shoe care kit." Keep it to 3 items maximum. More than that triggers decision paralysis.
Loyalty program introduction: If you run a points or tier-based loyalty program, this email announces the points they earned from their purchase and what they can redeem. This works especially well for consumable products where the reorder window is predictable.
interface CrossSellProps {
customerName: string;
purchasedProduct: string;
recommendations: {
name: string;
image: string;
price: number;
url: string;
}[];
loyaltyPoints?: {
earned: number;
total: number;
nextReward: string;
pointsNeeded: number;
};
}
function ProductCard({
product,
}: {
product: CrossSellProps["recommendations"][0];
}) {
return (
<Column style={{ width: "33%", padding: "0 8px", textAlign: "center" }}>
<a href={product.url} style={{ textDecoration: "none", color: "inherit" }}>
<Img
src={product.image}
width={120}
height={120}
alt={product.name}
style={{ borderRadius: "8px", margin: "0 auto" }}
/>
<Text
style={{
fontSize: "14px",
fontWeight: 600,
margin: "8px 0 4px",
color: "#1a1a1a",
}}
>
{product.name}
</Text>
<Text style={{ fontSize: "14px", color: "#6b7280", margin: 0 }}>
${product.price.toFixed(2)}
</Text>
</a>
</Column>
);
}Wiring the flow to your fulfillment system
Each email in this sequence maps to a webhook event from your e-commerce platform. Shopify, WooCommerce, BigCommerce, and most headless commerce APIs emit these events natively. The pattern is the same regardless of provider:
import { Resend } from "resend";
import OrderConfirmation from "@/emails/order-confirmation";
import ShippingNotification from "@/emails/shipping-notification";
import DeliveryConfirmation from "@/emails/delivery-confirmation";
const resend = new Resend(process.env.RESEND_API_KEY);
type FulfillmentEvent =
| "checkout.completed"
| "fulfillment.shipped"
| "fulfillment.delivered"
| "refund.processed";
const EMAIL_MAP: Record<FulfillmentEvent, React.ComponentType<any>> = {
"checkout.completed": OrderConfirmation,
"fulfillment.shipped": ShippingNotification,
"fulfillment.delivered": DeliveryConfirmation,
"refund.processed": RefundConfirmation,
};
export async function POST(req: Request) {
const event = await req.json();
const Template = EMAIL_MAP[event.type as FulfillmentEvent];
if (!Template) return new Response("Unknown event", { status: 200 });
await resend.emails.send({
from: "orders@yourstore.com",
to: event.customer.email,
subject: getSubjectLine(event.type, event.data),
react: <Template {...event.data} />,
});
return new Response("OK", { status: 200 });
}Building these six templates from scratch - typed props, responsive layouts, dark mode, cross-client testing - takes most teams 2-3 weeks. That's 2-3 weeks before the flow starts earning money. Pre-built e-commerce email templates with all six emails ready to customize cut that to an afternoon.
- Build all 6 post-purchase emails before launch: order confirmation, shipping, delivery, review request, refund, and cross-sell
- Fire emails 1-3 and 5 from fulfillment webhook events; schedule emails 4 and 6 on time delays after delivery
- Use typed props for every template so your commerce platform data maps cleanly to the email without runtime surprises
- Time review requests 3-5 days after delivery - earlier and the customer has not used the product, later and the excitement has faded
- Include a win-back offer in refund emails to convert returns into future purchases
- Pre-built e-commerce templates with dark mode, responsive layouts, and TypeScript props ship the entire flow in hours, not weeks