GeneratorsWooCommerceWooCommerce Email

WooCommerce Email Generator

A WC_Email subclass plus its own HTML template — with template_base set so wc_get_template_html() actually finds the plugin's file instead of falling through to core's.

class-acme-loyalty-earned-email.php
Saved just now
The email
Acme_Loyalty_Earned_Email
Recipient
Trigger
Template intro line
<?php
// Generated with WP CodeKit — powered by GrowQuest (https://growquest.io).
/**
 * Plugin Name:       Loyalty points earned email
 * Description:       Registers the acme_loyalty_earned transactional email.
 * Version:           1.0.0
 * Requires PHP:      7.4
 * Requires Plugins:  woocommerce
 * Text Domain:       acme
 */

defined( 'ABSPATH' ) || exit;

/**
 * Declare the class once WooCommerce's own email base class exists.
 */
function acme_email_init() {
	if ( class_exists( 'Acme_Loyalty_Earned_Email' ) ) {
		return;
	}

	class Acme_Loyalty_Earned_Email extends WC_Email {

	public function __construct() {
		$this->id             = 'acme_loyalty_earned';
		$this->customer_email = true;
		$this->title          = __( 'Loyalty points earned', 'acme' );
		$this->description    = __( 'Sent to the customer once their order is marked complete, showing the points they earned.', 'acme' );
		$this->heading        = __( 'You earned loyalty points!', 'acme' );
		$this->subject        = __( 'You earned points on order #{order_number}', 'acme' );

		$this->template_html  = 'emails/acme-loyalty-earned.php';
		$this->template_plain = 'emails/plain/acme-loyalty-earned.php';
		$this->template_base  = plugin_dir_path( __FILE__ ) . 'templates/';

		add_action( 'woocommerce_order_status_completed', array( $this, 'trigger' ), 10, 2 );

		parent::__construct();

	}

	/**
	 * The settings fields, keyed by option name.
	 */
	public function init_form_fields() {
		$this->form_fields = array(
		'enabled' => array(
			'title'   => __( 'Enable/Disable', 'acme' ),
			'type'    => 'checkbox',
			'label'   => __( 'Enable this email notification', 'acme' ),
			'default' => 'yes',
		),

		'subject' => array(
			'title'       => __( 'Subject', 'acme' ),
			'type'        => 'text',
			'default'     => '',
			'placeholder' => __( 'You earned points on order #{order_number}', 'acme' ),
			'desc_tip'    => true,
		),

		'heading' => array(
			'title'       => __( 'Email heading', 'acme' ),
			'type'        => 'text',
			'default'     => '',
			'placeholder' => __( 'You earned loyalty points!', 'acme' ),
			'desc_tip'    => true,
		),
		);
	}

	/**
	 * Fires on woocommerce_order_status_completed. Sends the email if it is enabled and there is a recipient.
	 *
	 * @param int      $order_id Order ID.
	 * @param WC_Order $order    Order object, when core already has it loaded.
	 */
	public function trigger( $order_id, $order = false ) {
		$this->setup_locale();

		if ( $order_id && ! is_a( $order, 'WC_Order' ) ) {
			$order = wc_get_order( $order_id );
		}

		if ( is_a( $order, 'WC_Order' ) ) {
			$this->object    = $order;
			$this->recipient = $this->object->get_billing_email();
		}

		if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
			$this->restore_locale();
			return;
		}

		$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );

		$this->restore_locale();
	}

	/**
	 * The HTML version, rendered through the plugin's own template file.
	 */
	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
		);
	}

	/**
	 * The plain-text version.
	 */
	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' => false,
			'plain_text'    => true,
			'email'         => $this,
		),
			'',
			$this->template_base
		);
	}
	}
}
add_action( 'plugins_loaded', 'acme_email_init' );

/**
 * Register it so it appears under WooCommerce → Settings → Emails.
 *
 * @param array $emails Existing email classes, keyed by class name.
 * @return array
 */
function acme_add_email_class( $emails ) {
	$emails['Acme_Loyalty_Earned_Email'] = new Acme_Loyalty_Earned_Email();

	return $emails;
}
add_filter( 'woocommerce_email_classes', 'acme_add_email_class' );

About this generator

WooCommerce Email Generator Online

Build a WooCommerce custom email as a real WC_Email subclass — settings fields, a trigger() method, get_content_html() and get_content_plain() — plus the HTML template it renders and the woocommerce_email_classes registration that puts it in WooCommerce → Settings → Emails alongside the core notifications.

The Template tab holds the matching templates/emails/ file, with its own copy button, already calling the woocommerce_email_header, woocommerce_email_order_details and woocommerce_email_footer actions so the email inherits the store’s branding. Free to use, no account, nothing uploaded.

Reviewed Output tested on WordPress 6.8 · PHP 8.3

Why the WooCommerce email generator beats extending WC_Email by hand

WC_Email gives you a lot for free — the branded wrapper, the settings screen, the placeholder system — but only if four things line up: the class is registered on woocommerce_email_classes, parent::__construct() runs after your properties are set, the trigger hook actually fires, and $this->template_base points at your plugin. Miss the last one and wc_get_template_html() looks in the theme and then in WooCommerce’s own templates directory, finds nothing, and sends an empty email. This generator gets all four right.

template_base set to your own plugin

The constructor sets $this->template_base to plugin_dir_path( __FILE__ ) . 'templates/', and both content methods pass it as the fourth argument to wc_get_template_html(). Without it WooCommerce resolves the template against the theme and its own directory only, and an email with no template body is the result.

A trigger you choose, wired correctly

Pick an order status and the class hooks woocommerce_order_status_{status} with two accepted arguments, which WooCommerce fires itself on every transition. Pick a custom hook and you get the same wiring plus a comment showing the do_action() call your own code must make.

Customer and admin recipients handled differently

A customer email sets $this->customer_email = true and reads the address off the order with get_billing_email() inside trigger(). An admin email adds a Recipient(s) settings field instead and falls back to get_option( 'admin_email' ), because an admin email has no order address to read.

A trigger() that follows core’s own shape

It calls setup_locale(), resolves the order from either the id or the object WooCommerce passed, bails through restore_locale() when the email is disabled or has no recipient, and only then calls send() with the full five arguments.

Both content methods, HTML and plain text

get_content_html() and get_content_plain() are generated as a matching pair, each passing order, email_heading, sent_to_admin, plain_text and email to the template — the exact variables the WooCommerce email actions expect.

Timing warnings you would otherwise learn the hard way

A customer email triggered on pending or failed is flagged, because both fire before payment is confirmed. A custom trigger hook is flagged too, with a reminder that nothing will ever call trigger() unless your own code fires it.

How does the WooCommerce Email Generator work?

Four steps. Name the email, choose who gets it, choose what sends it, then export both files.

  1. 01

    Name the email

    Set the email id, the title and description shown in WooCommerce → Settings → Emails, and the default heading and subject. Subject and heading support WooCommerce placeholders such as {order_number} and {site_title}.

  2. 02

    Choose the recipient

    Customer or admin. Customer reads the billing email off the order; admin adds a Recipient(s) settings field with a comma-separated list and falls back to the site admin address.

  3. 03

    Choose the trigger

    Either an order status — the email then fires on WooCommerce’s own woocommerce_order_status_{status} action with no extra code — or a custom hook name that your plugin fires with do_action().

  4. 04

    Export both files

    Copy the class from the main tab and the template from the Template tab, and save the template at templates/emails/your-email-id.php inside the plugin so template_base resolves.

Worked example — the constructor of a customer email on order completion

The part that decides whether the email works at all: its id, its templates, its base path and its trigger. Note that parent::__construct() comes last.

public function __construct() {
	$this->id             = 'acme_loyalty_earned';
	$this->customer_email = true;
	$this->title          = __( 'Loyalty points earned', 'acme' );
	$this->description    = __( 'Sent when an order is marked complete.', 'acme' );
	$this->heading        = __( 'You earned loyalty points!', 'acme' );
	$this->subject        = __( 'You earned points on order #{order_number}', 'acme' );

	$this->template_html  = 'emails/acme-loyalty-earned.php';
	$this->template_plain = 'emails/plain/acme-loyalty-earned.php';
	$this->template_base  = plugin_dir_path( __FILE__ ) . 'templates/';

	add_action( 'woocommerce_order_status_completed', array( $this, 'trigger' ), 10, 2 );

	parent::__construct();
}

parent::__construct() is what loads the settings, applies the saved subject and heading, and registers the email with the settings screen — so every property it should be able to override has to be set before it runs, not after.

WooCommerce custom emails — frequently asked questions

What developers run into when adding a transactional email of their own.

Why does my custom WooCommerce email arrive empty?

Almost always because $this->template_base was never set. wc_get_template_html() looks in the active theme’s woocommerce/ folder, then in the path you pass as the fourth argument, then in WooCommerce’s own templates directory. A plugin template lives in none of those unless you point template_base at your plugin folder and pass it through — which is what the generated content methods do.

Why does my custom email not appear under WooCommerce → Settings → Emails?

The class has to be added to the array returned by the woocommerce_email_classes filter, keyed by class name and holding an instance, not a string. It also has to be defined by the time that filter runs, which is why the generated code declares it inside a plugins_loaded callback. If the class is defined but never registered, everything else in it is dead code.

How do I trigger a WooCommerce email on a custom order status?

WooCommerce fires woocommerce_order_status_{status} for every status transition, including custom ones, so a status of awaiting-pickup gives you woocommerce_order_status_awaiting-pickup. Hook trigger() to it with two accepted arguments — WooCommerce passes the order id and, in most paths, the order object. Choose the custom trigger option in this generator and the same wiring is produced for a hook of your own.

Which placeholders can I use in the subject and heading?

WC_Email runs the subject and heading through format_string(), which replaces {site_title}, {site_address}, {order_date} and {order_number} among others, and any additional placeholders you register in $this->placeholders. Use those rather than string concatenation, so a store owner editing the subject in the settings screen keeps the dynamic parts working.

Do I need both an HTML and a plain-text template?

Yes, if the store might be set to plain text or multipart in WooCommerce → Settings → Emails. get_content_plain() is called for those and would return nothing without a plain template, sending an empty message. The generator writes both template_html and template_plain paths; give the plain file the same content without markup.

How do I test a custom transactional email without placing real orders?

WooCommerce has a built-in email preview under WooCommerce → Settings → Emails, which renders your registered email with sample data. Beyond that, calling WC()->mailer() to make sure the classes are loaded and then firing your trigger hook manually with a real order id is the quickest end-to-end check. Use a mail-catching tool locally so nothing reaches a real customer.

Where do I paste the code from the WooCommerce Email generator?

You have three good options and one bad one. Best is a small site-specific plugin — the generator can output one for you, and it keeps working when you change theme. A code-snippets plugin is fine too. A child theme's functions.php works but ties the code to that theme. Never edit the parent theme: the next update overwrites it and your code is gone.

Can I use the generated code in client work or a commercial plugin?

Yes. The output is ordinary WordPress API calls that you configured — there is no licence attached to it and no attribution required. Use it in client sites, commercial plugins and themes, or products you sell. The generated file carries a short credit comment you are free to delete.

The other WooCommerce generators in the library, plus the WordPress tools most often used alongside this one.