
📝 Adding WooCommerce order notes to emails: a complete guide
A customer places an order, and you leave a note in the admin panel: you clarified the delivery date, added a tracking number, or simply wrote "call before shipping." But the customer doesn't see this. Order notes in WooCommerce live only in the admin panel by default, on the order editing sidebar. They don't appear in emails sent to the buyer.
The problem is solved with a single snippet in functions.php. No plugins, no template editing, just a clean hook that outputs notes in the email body. In practice, we've been using this approach on client stores for several years now, with no failures after WooCommerce updates. In this guide, we'll cover how WooCommerce stores order notes, which hook to use, and how to customize the output for your needs.
💡 Quick overview:
- How WooCommerce stores order notes: the
order_notecomment type and thewc_get_order_notes()function - Two approaches to email customization: template overrides or hooks, and when to choose which
- A ready-to-use snippet for displaying notes in the order completion email with a line-by-line breakdown
- Customization: styling the list, filtering only customer notes, supporting plain-text emails
- Testing via email preview in the admin panel, without spamming real customers
Step 1: How WooCommerce stores order notes
WooCommerce stores notes as WordPress comments of the order_note type. Every action on an order (status change, refund, admin note, or customer message) is recorded in the wp_comments table with the flag comment_type = 'order_note'.
The difference between internal and customer notes is determined by the is_customer_note meta field. If the "Visible in My Account" checkbox is enabled when adding a note, WooCommerce sets is_customer_note = true, and the buyer sees it in their account. Regular internal notes (is_customer_note = false) are not shown to the customer anywhere.
With the release of High Performance Order Storage (HPOS) in WooCommerce 8.2+, notes moved to a separate wp_wc_order_notes table. Direct SQL queries to wp_comments with HPOS enabled may return empty results. Therefore, instead of get_comments(), it's better to use wc_get_order_notes(), which works correctly regardless of the storage mode.
Step 2: Templates or hooks, which approach to choose
WooCommerce offers two paths for email customization: template overrides and action hooks. Each has its own use case.
Template overrides. You copy a file from woocommerce/templates/emails/ to yourtheme/woocommerce/emails/ and edit the HTML directly. The advantage is full control over markup. The disadvantage is that after major WooCommerce updates, your template may diverge from the current version, and emails may start breaking. Documentation on template structure is now at woocommerce.com, not the old docs.woothemes.com.
Hooks. You attach a callback to the woocommerce_email_order_meta action, and WooCommerce calls your function at the right place in the email. The advantage is that it doesn't depend on template versions and works out of the box after core updates. The disadvantage is that you're limited to the hook's position (it fires in the order metadata block, after the products table).
Criterion | Templates | Hooks |
|---|---|---|
Control over markup | Full | Within hook position |
Resistance to updates | Low | High |
Implementation complexity | Higher | Lower |
Best for | Complete email redesign | Adding a single block |
For our task of displaying notes, a hook is more than sufficient. If you need to completely redesign email layouts, use templates and plan for updating them every six months or so.
Step 3: Adding the code to functions.php
The woocommerce_email_order_meta hook fires in all WooCommerce transactional emails: order confirmation, completion, cancellation, invoice, and others. Four parameters are passed to the callback: the order object $order, the $sent_to_admin flag, the $plain_text flag, and the email object $email.
Place this code in your active theme's functions.php (or via the Code Snippets plugin, which is safer since it won't be lost when changing themes):
1 add_action( 'woocommerce_email_order_meta', 'sd_add_order_notes_to_email', 10, 4 ); 2 3 function sd_add_order_notes_to_email( $order, $sent_to_admin, $plain_text, $email ) { 4 // Don't break the plain-text version of the email 5 if ( $plain_text ) { 6 echo "\n\n" . esc_html__( 'Order Notes:', 'woocommerce' ) . "\n"; 7 8 $notes = wc_get_order_notes( array( 9 'order_id' => $order->get_id(), 10 'type' => 'customer', 11 ) ); 12 13 if ( empty( $notes ) ) { 14 echo esc_html__( 'No notes for this order.', 'woocommerce' ) . "\n"; 15 return; 16 } 17 18 foreach ( $notes as $note ) { 19 echo '- ' . wp_strip_all_tags( $note->content ) . "\n"; 20 } 21 return; 22 } 23 24 // HTML version: get only customer notes 25 $notes = wc_get_order_notes( array( 26 'order_id' => $order->get_id(), 27 'type' => 'customer', 28 ) ); 29 30 if ( empty( $notes ) ) { 31 return; 32 } 33 34 echo '<h2>' . esc_html__( 'Order Notes', 'woocommerce' ) . '</h2>'; 35 echo '<ul class="order-notes-list" style="list-style:none;padding:0;margin:0 0 24px;">'; 36 37 foreach ( $notes as $note ) { 38 $note_date = sprintf( 39 '%1$s at %2$s', 40 date_i18n( get_option( 'date_format' ), strtotime( $note->date_created ) ), 41 date_i18n( get_option( 'time_format' ), strtotime( $note->date_created ) ) 42 ); 43 44 printf( 45 '<li style="background:#f9f9f9;border-left:4px solid #7f54b3;padding:12px 16px;margin-bottom:10px;border-radius:0 4px 4px 0;">' 46 . '<div style="margin-bottom:4px;">%s</div>' 47 . '<small style="color:#888;">%s</small>' 48 . '</li>', 49 wp_kses_post( nl2br( $note->content ) ), 50 esc_html( $note_date ) 51 ); 52 } 53 54 echo '</ul>'; 55 }
Here's what happens line by line:
- Line 1. We register the hook with priority 10 and accept all 4 parameters. Without
10, 4, WooCommerce will pass only the first argument, and you'll get an error. - Lines 5-20. Handling plain-text emails. Some emails are sent in text format, so we output notes as a list with dashes, without HTML.
- Lines 24-28.
wc_get_order_notes()requests only customer notes (type => 'customer'). Remove the filter to show all notes, including internal ones (status changes, refunds). If the array is empty, we output nothing and don't clutter the email. - Lines 34-38. We format the date using
date_i18n(), which respects the site locale and date/time format settings. - Lines 40-48.
printf()outputs each note in a styled block: purple bar on the left, light background, date in gray below.
The code uses no external dependencies and is compatible with WooCommerce 3.2+ (the wc_get_order_notes function was introduced in this version) and HPOS (native support from 8.2+).
Step 4: Testing without spamming customers
After adding the code, don't wait for a real order to test. WooCommerce provides a built-in email preview: WooCommerce → Settings → Emails → click on any email → "Preview" button at the bottom. You'll see the email in both HTML and plain-text, using a real order (the most recent one is used). If there are no orders, create a test one manually.
What to check:
- A customer note with "Visible in My Account" enabled appears in the email;
- An internal note (without the checkbox) doesn't appear (if you kept
type => 'customer'); - The plain-text version of the email isn't broken (switch via the link below the preview);
- The notes block isn't duplicated in admin emails (add a check
if ( $sent_to_admin ) return;if needed).
If you're using Code Snippets, simply deactivate the snippet to roll back. If you edited functions.php, comment out or delete the code. No traces are left in the database.
⁉️🤔 Frequently asked questions
Can I display ALL notes, not just customer notes?
Yes. Replace
'type' => 'customer'with'type' => 'internal'to get only internal notes. Remove thetypeparameter entirely to get all notes. Keep in mind that internal notes may contain service information (refund IDs, amounts) that customers shouldn't see. For most stores,'type' => 'customer'is sufficient. If you want to separate the output, create two blocks: first customer notes with the heading "Order Comments," then internal notes under the heading "Fulfillment Details" (this is for admin emails, using the$sent_to_adminflag).
Does the code work for a specific email or all of them?
The
woocommerce_email_order_metahook fires in all transactional emails: Completed Order, Processing Order, Order on Hold, Customer Invoice, and so on. If you need to limit output to only the order completion email, wrap the code in a check:
1 > if ( 'customer_completed_order' !== $email->id ) { 2 > return; 3 > } 4 > ```
1
plaintext
1 2 plaintext
plaintext
plaintext
The full list of email IDs is available in the WooCommerce documentation: customer_completed_order · customer_processing_order · customer_on_hold_order · customer_invoice · customer_refunded_order · customer_new_account · new_order · cancelled_order · failed_order.
Is it safe to edit functions.php on a live site?
Editing
functions.phpdirectly is a risky approach. A syntax error (an extra bracket, an unclosed quote) will cause the entire site to show a white screen. It's safer to use the Code Snippets plugin, which lets you add PHP snippets via the admin panel and automatically disables code with fatal errors. An alternative is WPCode (free version on wordpress.org), which offers the same functionality plus conditional logic (admin only, specific pages only). If you still editfunctions.php, make a backup of the file and keep FTP access handy. A white screen is fixed by replacingfunctions.phpwith the original from your backup.
Notes are duplicating in emails, what did I do wrong?
Two likely causes. First: you added the code both in
functions.phpand via a snippets plugin, so the hook fires twice. Keep only one. Second: your theme or another plugin is already attaching a callback to this same hook. Check by searching the project code (grep -r "woocommerce_email_order_meta" wp-content/). If duplication is from a plugin, simply remove your code. If from a theme, wrap your callback inremove_action()beforeadd_action()to ensure only one handler runs.
## The bottom line: is it worth adding notes to customer emails
Communication transparency is one of the cheapest ways to reduce support workload. When a customer sees "Tracking number: X123456789, delivery June 15" in the email, they don't go to chat asking "where's my order?" One snippet, zero plugins, five minutes to implement, and the effect is measured in dozens of saved tickets per month.
If your emails are already customized via templates, embed the notes output directly in the template rather than using a hook. If you use an email customization plugin (Kadence, YayMail, ShopMagic), each has its own mechanism for adding dynamic blocks, so use that instead of code.
Try it with one email type (Completed Order), test on a test order, and after a week evaluate whether you're getting fewer repetitive questions from customers. Almost certainly, yes.
1 2 plaintext
plaintext
1



