WooCommerce Post-Purchase Emails

The window right after a WooCommerce purchase is the highest-engagement moment a customer ever has with you. A well-timed thank-you / usage-tips / review-request / cross-sell sequence quietly increases LTV without any additional ad spend. This guide walks the hook setup, the 4-step sequence template, the product-category tagging trick, and the AcelleMail automation that ties it all together.

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

11 Kommentare

5 Kommentare

  1. tranminh.devop…
    Any plans for a native Shopify app? The webhook approach works but a real app integration would be smoother for non-technical users.
    1. admin
      There's no built-in way today. Two workarounds: (1) cron + custom script polling the API every N minutes, (2) webhook-driven if your event source supports it. Most operators go with #2...
    2. admin (bearbeitet)
      same answer as above fo saas-tenant — works the same way per-tenant, with the caveat that the cron must be set per-customer (not just system-wide).
  2. hung.nguyen.it
    if your webhook receiver is unreliable, point AcelleMail at an intermediate proxy with retry logic (Cloudflare Worker works well).
    1. admin
      Good tip. The Cloudflare-outbound-rate-limit case is something we hadn't documented.
  3. lucas.bernard.…
    WooCommerce integration finally documented properly. Was reverse-engineering the webhook payload before this.
    1. admin (bearbeitet)
      Thanks. Pass it along if it helps your team.
  4. aditi.s.bom
    Set up the Zapier bridge lst week. ~30 minutes from start to working flow. Cleaner than I expected.
  5. ahmed.hassan.c…
    Set up the Zapier bridge last week. ~30 minutes from start to working flow. Cleaner than I expected 👀
    1. admin
      Confirming your experience matches what we see in support cases. We'll cite the cause-#5 'wait it out' guidance more prominently in the next revision
    2. admin (bearbeitet)
      Useful field report. The 'kill -9 was the only fix' edge case is rare but real — we'll note it as a fallback.

More in Integrations