What this is for#
The window right after a WooCommerce purchase is the highest-engagement moment a customer ever has with you. Welcome them well and they convert again at 3-5× the baseline rate. Ignore them and you've left LTV on the table.
This guide walks the production setup: WooCommerce hook → AcelleMail list → automation flow. Pattern 2 from the WordPress + WooCommerce integration overview.
Prerequisites#
- A working WordPress + WooCommerce store
- A working AcelleMail install with an API token configured
- An AcelleMail list dedicated to post-purchase subscribers (separate from your general newsletter — different sequence, different opt-in semantics)
- An automation flow set up in AcelleMail to consume the events (we'll cover the template below)
Step 1 — The WooCommerce hook#
The right hook for post-purchase is woocommerce_order_status_completed — fires after payment AND fulfilment. NOT woocommerce_thankyou (fires on the thank-you page render even before payment confirms) or woocommerce_payment_complete (fires on payment but before fulfilment, can trip on subscriptions).
In a mu-plugin (per WordPress Subscriber Sync Step 2):
<?php
/**
* Plugin Name: AcelleMail WooCommerce Post-Purchase
* Description: Hook WooCommerce order completion to AcelleMail post-purchase list
*/
if (!defined('ACELLEMAIL_API_TOKEN') ||
!defined('ACELLEMAIL_POST_PURCHASE_LIST_UID') ||
!defined('ACELLEMAIL_BASE_URL')) {
return;
}
add_action('woocommerce_order_status_completed', function ($order_id) {
$order = wc_get_order($order_id);
if (!$order) return;
$email = $order->get_billing_email();
if (!is_email($email)) return;
// Build category tags for personalisation in the automation
$tags = [];
foreach ($order->get_items() as $item) {
$cats = get_the_terms($item->get_product_id(), 'product_cat');
if ($cats) foreach ($cats as $c) $tags[] = $c->slug;
}
$tags = array_unique($tags);
// Pick the "headline" product (highest-value line item) for the email subject merge
$headline_product = '';
$top_subtotal = 0;
foreach ($order->get_items() as $item) {
if ($item->get_subtotal() > $top_subtotal) {
$top_subtotal = $item->get_subtotal();
$headline_product = $item->get_name();
}
}
wp_remote_post(ACELLEMAIL_BASE_URL . '/api/v1/subscribers', [
'headers' => [
'Authorization' => 'Bearer ' . ACELLEMAIL_API_TOKEN,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
'body' => wp_json_encode([
'MAIL_LIST_UID' => ACELLEMAIL_POST_PURCHASE_LIST_UID,
'EMAIL' => $email,
'FIRST_NAME' => $order->get_billing_first_name(),
'LAST_NAME' => $order->get_billing_last_name(),
'ORDER_ID' => $order_id,
'ORDER_TOTAL' => $order->get_total(),
'ORDER_CURRENCY' => $order->get_currency(),
'HEADLINE_PRODUCT' => $headline_product,
'PRODUCT_CATEGORIES' => implode(',', $tags),
'PURCHASE_DATE' => $order->get_date_completed() ? $order->get_date_completed()->format('Y-m-d') : date('Y-m-d'),
]),
'timeout' => 10,
'blocking' => false,
]);
});
Add the custom fields to the AcelleMail list (Lists → your post-purchase list → Fields → Add) before pushing — the API won't reject unknown fields, but they're discarded.
Step 2 — Set up the AcelleMail list#
In AcelleMail → Lists → New:
- Name: Customers (Post-Purchase)
- Default subject prefix:
[Your Store]
- Custom fields:
ORDER_ID (number)
ORDER_TOTAL (decimal)
ORDER_CURRENCY (text, 3 chars)
HEADLINE_PRODUCT (text, 200 chars)
PRODUCT_CATEGORIES (text, 500 chars; comma-separated)
PURCHASE_DATE (date)
- Opt-in: Single (purchase IS implicit opt-in for transactional follow-up)
The reason to use a separate list (not your general newsletter): different cadence, different unsubscribe behaviour, different compliance status. A newsletter unsubscribe shouldn't stop post-purchase emails about an active order.
Step 3 — The 4-step automation sequence#
In AcelleMail → Automation → New Workflow on this list:
| Email |
Delay |
Subject example |
Goal |
| 1. Thank you + setup |
Immediately |
Welcome, {FIRST_NAME} — your {HEADLINE_PRODUCT} ships soon |
Confirm receipt; set delivery expectations; link to setup guides |
| 2. Usage tips |
Day 3 |
Getting the most from your {HEADLINE_PRODUCT} |
Drive activation; reduce returns; build product confidence |
| 3. Review request |
Day 7 |
How's your {HEADLINE_PRODUCT}? Quick favour? |
Collect reviews; social proof for future buyers |
| 4. Related products |
Day 14 |
You might also like these... |
Cross-sell; based on PRODUCT_CATEGORIES tag |
This is the proven 4-step template — see Post-Purchase Follow-Up Automation for the full content templates.
Personalising per product category#
Inside the automation, branch on PRODUCT_CATEGORIES:
- "If
PRODUCT_CATEGORIES contains coffee" → use coffee-specific templates (related products, brewing tips)
- "If
PRODUCT_CATEGORIES contains electronics" → use electronics templates (compatibility tips, warranty registration)
- Otherwise → use generic templates
Acelle's automation builder supports conditional branching — see the advanced triggers guide.
When the customer buys again#
If the customer makes a second purchase before completing the 4-step sequence, they're re-added to the list (Acelle handles as upsert) and the automation may re-evaluate. To prevent customers from getting "How's your X?" review-request emails for a product they bought last week, add an automation step: "If purchases in last 7 days > 1, skip" or use a re-entry cooldown.
Step 4 — Handle refunds / cancellations#
When an order is refunded, you may want to pause the automation for that customer (the day-7 review email feels off if they just got a refund):
add_action('woocommerce_order_status_refunded', function ($order_id) {
$order = wc_get_order($order_id);
if (!$order) return;
// Remove from post-purchase list to stop the automation
// First find subscriber by email:
$find = wp_remote_get(
ACELLEMAIL_BASE_URL . '/api/v1/subscribers/email/' . urlencode($order->get_billing_email()),
['headers' => ['Authorization' => 'Bearer ' . ACELLEMAIL_API_TOKEN, 'Accept' => 'application/json']]
);
$sub = json_decode(wp_remote_retrieve_body($find), true);
if (!$sub || empty($sub['id'])) return;
// Tag them as refunded (the automation can branch on this tag)
wp_remote_post(
ACELLEMAIL_BASE_URL . '/api/v1/subscribers/' . $sub['id'] . '/add-tag',
[
'headers' => ['Authorization' => 'Bearer ' . ACELLEMAIL_API_TOKEN, 'Accept' => 'application/json', 'Content-Type' => 'application/json'],
'body' => wp_json_encode(['tag' => 'refunded:' . $order_id]),
]
);
});
Then in the automation: "If tag = refunded, exit flow".
Common issues#
| What you see |
Likely cause |
Fix |
| Sequence fires twice for the same order |
Order moved through completed status twice (e.g. manual admin retrigger) |
Check tag for order-id:{order_id}; skip if already tagged |
| Review emails sent to refunded customers |
No refund hook configured |
Add Step 4 refund handler above |
| Custom fields not appearing in email merges |
Custom fields not added to the AcelleMail list, OR field names lowercase |
Add UPPERCASE field names in AcelleMail Lists → Fields BEFORE pushing |
| Customer complains about getting Day 14 email after a 30-day window passed |
Slow queue / paused worker |
Verify supervisor + queue per queue setup guide |
| Hook doesn't fire on subscription renewals |
WC Subscriptions uses different hooks |
Add add_action('woocommerce_subscription_renewal_payment_complete', ...) for renewals |
Tag with comma in name (product_cat slug has comma) |
implode(',', $tags) produces ambiguous joining |
Use ; separator instead, or filter out commas in slugs first |
| Customer base going up but automation engagement drops |
Including non-customer registrations in same automation |
Verify the post-purchase hook posts to the dedicated list (not the general newsletter) |
FAQ#
WooCommerce Subscriptions hooks? woocommerce_subscription_renewal_payment_complete fires on each renewal — add the same body to push to a subscriber-renewals list. Different sequence (skip "first review" since they already bought before; emphasize loyalty rewards).
WooCommerce Memberships hooks? wc_memberships_user_membership_status_changed — branch on $new_status to subscribe to relevant lists ("new member" vs "expired" vs "renewed").
Can I unsubscribe customers from the post-purchase list after Day 14? Add a final "Remove from list" step at the end of the automation. Or use a "Move to" step to add them to your general newsletter (they're now a paying customer; appropriate to nurture them long-term).
What about transactional emails (order confirmation, shipping notification)? Keep those in WooCommerce — that's its job, and they need to be 100% reliable per-order. The AcelleMail flow is for marketing follow-up.
Can I include the order line items in the welcome email? Yes — fetch $order->get_items() and pass them as a JSON-encoded LINE_ITEMS custom field, then parse in the AcelleMail template via merge tags + a small JS rendering. For most stores, just naming the HEADLINE_PRODUCT (the highest-value line) is enough.
How do I A/B test the post-purchase sequence? AcelleMail supports automation-level A/B (split subscribers 50/50, send different sequences). See A/B Testing Email Subject Lines for the principles.
Multilingual store? Pass the order's locale ($order->get_meta('_locale') if set, else get_locale()) as a custom field. Branch the automation on it to send the right-language sequence.
Related articles#