The challenge of getting emails right in WooCommerce
WooCommerce transactional emails are often the most visible touchpoint your customers have with your brand outside of your storefront. When an order confirmation arrives late, lacks branding, or worse, never arrives at all, you've lost an opportunity to reinforce trust and guide the customer through their purchase journey. WooCommerce emails: WordPress best practices explained covers the strategies we've used across production stores to take control of these critical communications.
Whether you're running a high-volume e-commerce operation or a specialized store, the default WooCommerce email templates rarely fit perfectly. You'll need to customize them, extend them with new email types, and ensure they actually reach your customers' inboxes. We'll walk through the practical patterns that work.
Override email templates
WooCommerce emails are rendered using template files, and the cleanest way to customize them without losing your changes on plugin updates is to override the templates in your theme or a custom plugin.
The email template hierarchy works like this: WooCommerce looks first in your theme's woocommerce/emails/ directory, then falls back to the plugin's templates. To override a template, copy it from wp-content/plugins/woocommerce/templates/emails/ to your theme or custom plugin folder, maintaining the same directory structure.
Let's say you want to customize the order confirmation email. First, identify which template controls it. For most order-related emails, you'll work with customer-completed-order.php or similar. Create the path in your theme:
your-theme/
└── woocommerce/
└── emails/
└── customer-completed-order.phpInside that file, you have access to the $order object and can add custom branding, messaging, or logic:
<?php
/**
* Customer completed order email
*/
do_action( 'woocommerce_email_header', $email_heading, $email );
echo '<p>Thank you for your order! Your order details are below.</p>';
// Custom section: Add loyalty points info
$loyalty_points = get_post_meta( $order->get_id(), '_loyalty_points_earned', true );
if ( $loyalty_points ) {
echo '<p><strong>You earned ' . esc_html( $loyalty_points ) . ' loyalty points!</strong></p>';
}
do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email );
do_action( 'woocommerce_email_footer', $email );
?>The key benefit here is that your overrides persist through WooCommerce updates. When a new version lands, your custom templates stay intact because they live in your theme or plugin, not in WooCommerce itself.
However, this approach works best for tweaks to existing emails. If you need more sophisticated customization—conditional blocks, database queries, or multi-step logic—consider using an action hook within the template instead of cluttering the template file itself. Use hooks like woocommerce_email_order_details or woocommerce_email_footer to inject custom content from your plugin.
Add custom emails by extending WC_Email
Default WooCommerce emails cover the standard flow: new order, processing, completed, cancelled, and so on. But many stores need custom emails—reorder reminders, abandoned cart notices, shipping delay alerts, or special promotions triggered by specific conditions.
The right way to add a new email type is to extend the WC_Email class. This gives you full control over when the email fires, what data it receives, and how it renders.
Here's a practical example: a custom email that fires when an order contains a particular product category and offers a follow-up discount:
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WC_Email_Custom_Offer extends WC_Email {
public function __construct() {
$this->id = 'wc_email_custom_offer';
$this->title = __( 'Custom Offer Email', 'woocommerce' );
$this->description = __( 'Sent after an order with premium items', 'woocommerce' );
$this->heading = __( 'Here\'s a special offer just for you', 'woocommerce' );
$this->subject = __( 'Complete your collection – exclusive discount inside', 'woocommerce' );
// Email template location
$this->template_html = 'emails/custom-offer.php';
$this->template_plain = 'emails/plain/custom-offer.php';
parent::__construct();
// Hook to trigger this email
add_action( 'woocommerce_order_status_completed_notification', array( $this, 'trigger' ), 10, 2 );
}
public function trigger( $order_id, $order = null ) {
if ( ! $order_id ) {
return;
}
if ( ! is_a( $order, 'WC_Order' ) ) {
$order = wc_get_order( $order_id );
}
if ( ! $order ) {
return;
}
// Only send if order contains our premium category
$has_premium = false;
foreach ( $order->get_items() as $item ) {
$product = $item->get_product();
if ( has_term( 'premium', 'product_cat', $product->get_id() ) ) {
$has_premium = true;
break;
}
}
if ( ! $has_premium ) {
return;
}
$this->object = $order;
$this->recipient = $order->get_billing_email();
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return;
}
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
public function get_content_html() {
return wc_get_template_html( $this->template_html, array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => false,
'plain_text' => false,
'email' => $this,
), '', $this->template_base );
}
public function get_content_plain() {
return wc_get_template_html( $this->template_plain, array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
'sent_to_admin' => true,
'plain_text' => true,
'email' => $this,
), '', $this->template_base );
}
public function get_default_subject() {
return $this->subject;
}
public function get_default_heading() {
return $this->heading;
}
}
?>Register this class in your custom plugin's init hook:
add_filter( 'woocommerce_email_classes', function( $emails ) {
require_once dirname( __FILE__ ) . '/includes/class-wc-email-custom-offer.php';
$emails['WC_Email_Custom_Offer'] = new WC_Email_Custom_Offer();
return $emails;
} );Then create your template file at your-theme/woocommerce/emails/custom-offer.php. The custom email class gives you complete ownership: you decide the trigger, the recipient, the content, and the styling. This scales far better than hardcoding email logic into hooks.
Use an SMTP or API provider
By default, WordPress uses the server's mail() function, which is unreliable in production. Emails get queued on the server, often hit spam filters, and you have no visibility into delivery. For any serious store, use a dedicated SMTP service or API provider.
Popular options include Sendinblue, AWS SES, Mailgun, and Google Workspace SMTP. We typically recommend AWS SES for high volume or Sendinblue for simplicity. The setup is straightforward with a plugin like WP Mail SMTP:
- Install and activate WP Mail SMTP from the plugin directory.
- Go to Settings > WP Mail SMTP and select your mailer (Sendgrid, AWS SES, etc.).
- Enter your API credentials or SMTP details.
- Set the sender email to match your domain (critical for deliverability).
- Run a test email to confirm.
If you're integrating programmatically, WP Mail SMTP exposes hooks you can use:
add_filter( 'wp_mail', function( $args ) {
// Ensure WooCommerce emails use our configured SMTP
if ( strpos( $args['message'], 'order' ) !== false ) {
$args['headers'][] = 'X-Priority: 1';
}
return $args;
} );Beyond the plugin, consider logging email sends to your database. When a customer says they never received an order confirmation, you need proof that the email was dispatched and whether the provider reported a bounce or rejection. Use a hook to log the attempt:
add_action( 'wp_mail_succeeded', function( $mail_data ) {
error_log( 'Email sent to: ' . $mail_data['to'] . ' | Subject: ' . $mail_data['subject'] );
} );Test with a mail catcher
Before deploying to production, you need a safe way to test emails without spamming real customers. A mail catcher intercepts emails sent from your development or staging environment and displays them in a web interface. This lets you verify content, formatting, and links without touching your email provider's quota.
Popular mail catchers include Mailhog, MailpIT, and the service Mailtrap. For local development, Mailhog is free and easy to run in Docker:
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhogThen configure WP Mail SMTP to send to localhost:1025. Any email sent from WordPress gets captured, and you view it at http://localhost:8025. You can inspect headers, verify attachments, and test responsive HTML rendering all without a real email account.
For staging environments, use Mailtrap. It's a cloud-based mail catcher with an SMTP endpoint and API. Set WP Mail SMTP to use Mailtrap's credentials, and all emails are captured in a secure inbox. Team members can review them, and you get delivery reports. This workflow prevents accidental sends to production customers during testing.
Also integrate email testing into your deployment checklist. Before pushing a custom email class to production, verify:
- The email fires at the expected trigger event.
- All template variables render correctly (no undefined index warnings).
- Links point to the right URLs on the production domain.
- HTML renders correctly in major email clients (Outlook, Gmail, Apple Mail).
- Plain text fallback is readable.
Common mistakes
Hardcoding email addresses: Never hardcode admin email addresses or send emails directly without respecting WooCommerce's email recipient settings. Always use $order->get_billing_email() and check if the email type is enabled via is_enabled().
Forgetting plain text versions: Many developers build beautiful HTML emails but skip the plain text template. Email clients and accessibility tools rely on plain text fallbacks. Always provide both.
Not respecting customer preferences: If a customer unsubscribes from marketing emails, don't send them promotional follow-ups. Transactional emails (order confirmation, shipping notice) are exempt, but use hooks to honor preferences for custom emails that lean promotional.
Template files with side effects: Email templates should only output content. Don't update post meta, send additional emails, or trigger external API calls from inside a template file. Use action hooks in your custom email class instead.
Ignoring deliverability: Even with custom emails perfectly crafted, they'll land in spam if you don't set up SPF, DKIM, and DMARC on your domain. Work with your email provider to configure these DNS records. Test with Mail-tester.com to catch issues before they hit production.
Key takeaways
- Override WooCommerce email templates by placing copies in your theme's
woocommerce/emails/directory to persist through updates. - Extend
WC_Emailfor custom email types; this gives you programmatic control over triggers, recipients, and content without polluting template files. - Use a production-grade SMTP or API provider (AWS SES, Sendgrid, Sendinblue) and configure it via WP Mail SMTP; don't rely on server
mail()for deliverability. - Test all emails in a mail catcher (Mailhog locally, Mailtrap on staging) before production to verify content, links, and rendering.
- Always provide plain text email templates alongside HTML, respect customer unsubscribe preferences, and never execute business logic inside template files.
- Configure SPF, DKIM, and DMARC on your domain to maximize inbox delivery and prevent spam folder placement.






Comments
Loading comments…