Email customisation
How email actually moves through the plugin, where you can step in, and the patterns for the most common things you’ll want to do.
How the email pipeline works (from save to inbox)
Before you start customising anything, get the path straight in your head. A customer note is the easiest example.
- An admin writes a customer note and hits send. The REST endpoint saves the note to the database.
- Immediately, the endpoint calls
AsyncJob::queue()to schedule the email as an Action Scheduler job. The endpoint then returns. The admin sees their message in the thread within a hundred milliseconds — they’re not waiting on SMTP. - A few seconds later, Action Scheduler runs the job. The job fires
HOOK_CUSTOMER_NOTIFICATIONwith two args:update_idandnote_id. - The plugin’s
CustomerOrderUpdateEmailclass (aWC_Emailsubclass) is attached to that hook. It loads the note from the database, builds the HTML by including its template, and callswp_mail(). - The plugin marks the note as
notified_at = NOW()so it doesn’t accidentally re-send.
Every plugin email follows this shape — the hook name and the email class change, but the queue / fire / build / send sequence is identical. Knowing where you can step in is half the battle.
List of all emails the plugin sends
| Class | Goes to | Fires when |
|---|---|---|
CustomerOrderUpdateEmail | Customer | Customer-visible note added, or update status changed |
CustomerRatingRequestEmail | Customer | Update is marked solved (prompts the rating) |
CustomerRatingFollowupEmail | Customer | After the customer rates — promoter share or detractor empathy |
CustomerUpdateDeletedEmail | Customer | An update they could see is deleted, with “notify” chosen |
AdminOrderUpdateEmail | Site admin | Detractor rating arrives, or a customer-initiated update is created |
AssigneeOrderUpdateEmail | Assignee | Assignment, reassignment, customer reply, customer rates the update |
InternalMentionEmail | @mentioned user | You were tagged in an internal note |
ParticipantUpdateEmail | Participants | New note in a thread you’ve already contributed to |
CreatorUpdateDeletedEmail | Update creator | Another staff member deleted your update |
Change subject, heading, or body text
If you only want different wording, stop reading this developer page and go to WooCommerce → Settings → Emails. Every plugin email is in that list. Click any one and you get the standard WooCommerce email customiser — edit subject, heading, additional content, recipient, the works. No code, no theme files, survives plugin updates.
Subject lines support the standard WooCommerce tokens ({site_title}, {order_number}) plus a couple of plugin-specific ones documented inline next to each email’s default text.
The rest of this page is for the cases where the customiser isn’t enough.
Override the email HTML template (theme override)
The customiser changes the subject and body text. It doesn’t change the HTML structure — the colours, the header logo, the button styles. For that you copy the template into your theme and edit it there.
Three template files cover every plugin email:
# Customer-facing emails (notes, rating requests, etc.)
cp wp-content/plugins/order-updates-for-woo/src/Frontend/Notifications/Templates/order-update-notification.php \
wp-content/themes/your-theme/order-updates-for-woo/frontend/notifications/order-update-notification.php
# Promoter / detractor follow-up
cp wp-content/plugins/order-updates-for-woo/src/Frontend/Notifications/Templates/rating-followup.php \
wp-content/themes/your-theme/order-updates-for-woo/frontend/notifications/rating-followup.php
# Staff-facing emails (assignee, @mention, participant, etc.)
cp wp-content/plugins/order-updates-for-woo/src/Admin/Notifications/Templates/order-update-notification.php \
wp-content/themes/your-theme/order-updates-for-woo/admin/notifications/order-update-notification.php
Edit your copy. Reload. View::render() picks up the theme version automatically — no filter to register, no settings to flip. Delete the override and behaviour falls back to the plugin’s default.
Heads up: the templates are pure PHP. They receive the email’s context as locally-scoped variables ($order, $update, $note_content, $action_url, etc.). Open one and read it before you edit — the available variables are obvious from the top of the file.
Suppress or disable an email
Plenty of stores already have a CRM or helpdesk sending customer notifications — the plugin’s emails would just duplicate them. Disable a specific one without touching anything else.
Easiest path: WooCommerce → Settings → Emails → <the email>, untick “Enable this email notification”, save. Done.
If you want the toggle to be programmatic — turn it off in code so a non-technical user doesn’t accidentally re-enable it — use WooCommerce’s standard filter:
use OrderUpdatesForWoo\Shared\Config\Constants;
add_filter(
'woocommerce_email_enabled_' . Constants::EMAIL_ID_CUSTOMER_RATING_REQUEST,
'__return_false'
);
This is the same filter WC uses for every email; nothing plugin-specific about it. The trick is the email ID — use the constant from OrderUpdatesForWoo\Shared\Config\Constants, not the raw string. The string can change between versions; the constant is the stable identifier.
Intercept the email dispatch (run custom code per email)
Say you’ve got a CRM — Hubspot, Pipedrive, your own — and you want every customer-facing message to show up on the contact’s timeline. Not instead of the email. In addition.
This is what HOOK_CUSTOMER_NOTIFICATION is for. It fires inside the Action Scheduler job, just before wp_mail() goes out. You attach a named handler at a lower priority and do your CRM work there.
use OrderUpdatesForWoo\Shared\Config\Constants;
add_action( Constants::HOOK_CUSTOMER_NOTIFICATION, 'my_addon_log_to_crm', 5, 1 );
function my_addon_log_to_crm( array $args ): void {
$note = OrderUpdatesForWoo\Plugin::db()->get_customer_note( $args['note_id'] );
if ( ! $note ) {
return;
}
My_Addon\Crm::log_outbound_message( [
'order_id' => (int) $note['order_id'],
'body' => $note['note'],
'sent_at' => $note['created_at'],
] );
}
Your handler runs alongside the email, not instead of it. The customer still gets the email. If you want to replace the email entirely, you also need to disable it (see the previous section).
Priority 5 matters. WC_Email’s trigger runs at priority 10. Going at priority 5 means your CRM log fires before the email. If the CRM is down and your code throws, nothing’s been sent — fix it, retry the queue job. Run at priority 15 and the email’s already in the customer’s inbox while the CRM has no record — you’ll spend Monday reconciling.
The if ( ! $note ) check matters. Notes can be deleted between when the job queues and when it runs. If the note’s gone, your handler bails quietly rather than crashing the whole queue job.
Route email through a different SMTP provider
You don’t replace anything in this plugin to switch transactional providers. Install an SMTP/API plugin like WP Mail SMTP or Post SMTP, configure it once with your provider’s credentials, and every wp_mail() call on the site — including this plugin’s — routes through the new sender.
This is the right answer 99% of the time. WP Mail SMTP knows about email delivery, signed sending domains, bounce handling, retry logic. Don’t reinvent any of that inside a plugin hook.
If you genuinely need per-email routing — some emails through Postmark, others through SES — you can branch inside phpmailer_init (a WordPress core filter, not plugin-specific) and reconfigure the mailer based on the subject or recipient. That’s a more advanced topic and lives outside this doc; the WP Mail SMTP docs have a good chapter on it.
Customise email per recipient (VIP, role-based)
Common request: customers with a VIP tag should get a different signature in their notification emails. Standard WC has nothing for this; you handle it inside the email’s template variables via a filter.
Every OrderUpdateEmailBase subclass passes a context array into View::render(). The template includes additional_content at the bottom — the same field the WC customiser exposes. Filter woocommerce_email_additional_content_<id> to swap that text based on the order or customer.
use OrderUpdatesForWoo\Shared\Config\Constants;
add_filter(
'woocommerce_email_additional_content_' . Constants::EMAIL_ID_CUSTOMER_UPDATE,
'my_addon_vip_signature',
10, 3
);
function my_addon_vip_signature( string $content, $object, $email ): string {
if ( ! $object instanceof \WC_Order ) {
return $content;
}
$is_vip = (string) $object->get_meta( '_is_vip' ) === 'yes';
if ( ! $is_vip ) {
return $content;
}
return $content . "\n\nThanks for being a VIP customer. Your dedicated account manager is Sarah -- reply to this email and it goes straight to her.";
}
This is standard WC filter territory — nothing plugin-specific except the email ID constant. Same pattern works for the heading (woocommerce_email_heading_<id>) and the subject (woocommerce_email_subject_<id>) if you need to vary those per recipient.
Add a brand new custom email type
Say you’ve added a new event — a Slack-to-update bridge that creates updates from Slack reactions — and you want a different-looking email for that case. Don’t fork the existing classes. Write your own WC_Email subclass and register it the same way the core plugin does.
declare(strict_types=1);
namespace My_Addon\Notifications\Emails;
use OrderUpdatesForWoo\Shared\Notifications\OrderUpdateEmailBase;
class SlackBridgedUpdateEmail extends OrderUpdateEmailBase {
public function __construct( \OrderUpdatesForWoo\Shared\Updates\OrderUpdatesDb $db ) {
$this->id = 'my_addon_slack_bridged_update';
$this->title = __( 'Slack-bridged update notification', 'my-addon' );
$this->description = __( 'Sent when an update is created from a Slack reaction.', 'my-addon' );
$this->customer_email = false; // staff-facing
parent::__construct( $db );
// Use your own template, or reuse the plugin's admin one.
$this->template_html = MY_ADDON_PATH . 'templates/slack-bridged-notification.php';
}
public function trigger( int $update_id, int $recipient_user_id ): bool {
$this->reset_trigger_state();
if ( ! $this->load_context( $update_id ) ) {
return false;
}
$user = get_userdata( $recipient_user_id );
$this->recipient = $user->user_email;
$this->intro_text = __( 'A new update was created from a Slack reaction.', 'my-addon' );
if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return false;
}
return $this->send_with_locale();
}
public function get_default_subject(): string {
return __( '[{site_title}] Slack-bridged update on #{order_number}', 'my-addon' );
}
public function get_default_heading(): string {
return __( 'Update from Slack', 'my-addon' );
}
}
// Register the class with WC.
add_filter( 'woocommerce_email_classes', 'my_addon_register_email_class' );
function my_addon_register_email_class( array $emails ): array {
$emails['my_addon_slack_bridged_update'] = new \My_Addon\Notifications\Emails\SlackBridgedUpdateEmail(
OrderUpdatesForWoo\Plugin::db()
);
return $emails;
}
Three things you get for free by extending OrderUpdateEmailBase: the locale dance (setup_locale / send / restore_locale already handled by send_with_locale()), the same template-override system the core emails use, and a row in WooCommerce → Settings → Emails for your email so non-developers can change the wording without touching code.
Fire your email from wherever its trigger lives — the moment your Slack handler creates the update, call $email->trigger( $update_id, $recipient_id ). If the work is heavy, queue it through Action Scheduler the same way the core plugin does.
Email ID constants (for the WC enabled/disabled filter)
Every WC email filter takes the email’s string ID. Always reach for the constant, never the literal string — the literals can change between versions, the constants stay stable.
| Constant | |
|---|---|
| Admin notification (low rating, guest-created update) | EMAIL_ID_ADMIN_UPDATE |
| Assignee notification | EMAIL_ID_ASSIGNEE_UPDATE |
| @mention notification | EMAIL_ID_INTERNAL_MENTION |
| Participant notification | EMAIL_ID_PARTICIPANT_UPDATE |
| Customer notification | EMAIL_ID_CUSTOMER_UPDATE |
| Rating request | EMAIL_ID_CUSTOMER_RATING_REQUEST |
| Rating follow-up | EMAIL_ID_CUSTOMER_RATING_FOLLOWUP |
| Customer update deleted | EMAIL_ID_CUSTOMER_UPDATE_DELETED |
| Creator update deleted | EMAIL_ID_CREATOR_UPDATE_DELETED |
All under OrderUpdatesForWoo\Shared\Config\Constants. Use the use statement once at the top of your file and reach for the constant by short name (Constants::EMAIL_ID_CUSTOMER_UPDATE) thereafter.