Order emails are one of the few things every customer actually reads. Adding a personal note, a shipping estimate, or a support contact to that email doesn’t need an email customizer plugin, WooCommerce’s own email templates expose hooks for exactly this.
The Code
Here’s a snippet that adds a custom message before the order table, and a support contact line after it, only on customer-facing emails:
add_action( 'woocommerce_email_before_order_table', 'fixelar_email_intro_message', 10, 4 );
function fixelar_email_intro_message( $order, $sent_to_admin, $plain_text, $email ) {
if ( $sent_to_admin ) {
return;
}
if ( $plain_text ) {
echo "\nThanks for shopping with us. Here's a summary of your order:\n\n";
} else {
echo '<p>' . esc_html__( "Thanks for shopping with us. Here's a summary of your order:", 'woocommerce' ) . '</p>';
}
}
add_action( 'woocommerce_email_after_order_table', 'fixelar_email_support_line', 10, 4 );
function fixelar_email_support_line( $order, $sent_to_admin, $plain_text, $email ) {
if ( $sent_to_admin ) {
return;
}
if ( $plain_text ) {
echo "\nQuestions? Reach us at [email protected]\n";
} else {
echo '<p>' . esc_html__( 'Questions? Reach us at ', 'woocommerce' ) .
'<a href="mailto:[email protected]">[email protected]</a></p>';
}
}
Where to Put This Code
This goes in your child theme’s functions.php, or in a snippets plugin like Code Snippets. Never place it in a parent theme, since a theme update will wipe it out.
Every hook here fires for every order-related email WooCommerce sends, unless you add a condition to target a specific one. If you only want the message on the “Completed” order email, not “Processing” or “On hold,” check $email->id inside your function, for example if ( 'customer_completed_order' !== $email->id ) { return; }.
How the Code Works, Line by Line
All four core email hooks (woocommerce_email_before_order_table, woocommerce_email_after_order_table, woocommerce_email_order_details, woocommerce_email_customer_details) pass the same four parameters: $order (the WC_Order object), $sent_to_admin (true if this copy is going to the store owner, not the customer), $plain_text (true if the email is being generated as plain text instead of HTML), and $email (the WC_Email object, which includes $email->id to identify which specific email is being built).
Checking $sent_to_admin first, and returning early if true, is what keeps a customer-facing message out of the admin’s copy of the same email. Skipping this check is the most common way a “thanks for shopping” note ends up in the store owner’s order notification too.
Checking $plain_text and branching your output is necessary because WooCommerce generates both an HTML and a plain-text version of every email, and some customers’ email clients render the plain-text version. HTML tags in the plain-text branch show up as literal text, so each branch needs its own version of the message.
woocommerce_email_before_order_table fires right before the order items table, woocommerce_email_after_order_table right after it, and woocommerce_email_customer_details near the billing and shipping details block. Which one you use depends entirely on where in the email you want the content to land.
Real Use Cases
A personal thank-you note or brand message. A short line before the order table is a common way to add warmth to what’s otherwise a transactional email.
Adding shipping or delivery estimates. If this information isn’t already part of your shipping method configuration, woocommerce_email_after_order_table is a natural place to add it.
A support contact or FAQ link. Reducing “where’s my order” tickets often starts with making the answer visible in the confirmation email itself.
Order-specific instructions for certain product types. If specific products need care instructions, activation codes, or setup steps, checking the order’s line items inside your hooked function lets you show that only when relevant, rather than on every email.
Common Issues
My custom content shows up in the admin’s copy too. This means the $sent_to_admin check is missing or not returning early correctly. Double-check the conditional is actually inside the function before any output happens.
The message appears in HTML emails but not plain-text ones, or vice versa. Both branches of the $plain_text conditional need their own version of the content. If only one branch has an echo statement, the other version of the email will be missing it entirely.
My hook fires but nothing shows up in the email. Confirm the argument count in add_action() matches what you’re using. These hooks pass 4 arguments, so add_action( 'hook_name', 'function_name', 10, 4 ) needs that 4 at the end, or WordPress won’t pass all four parameters to your function.
Links inside the custom content don’t work correctly. Some email clients strip or mangle links added through these hooks if the surrounding HTML isn’t well-formed. Testing the actual rendered email (not just previewing it in wp-admin) across at least one real inbox catches this before customers see it.
If you need order emails customized with dynamic content pulled from custom fields, specific product logic, or a fully redesigned template, that’s exactly the kind of work Fixelar handles as part of our WooCommerce custom development services. We test against real inboxes, not just the wp-admin preview.