Add back-in-stock email notifications to WooCommerce: a plugin you can build today
· 6 min read
WooCommerce tracks stock levels and sends low-stock emails to the store admin. It has no equivalent for customers. When a visitor arrives at an out-of-stock product page, WooCommerce shows the stock status and stops there. There is no sign-up field, no waiting list, and no automated email when you restock. The customer either checks back manually or leaves.
Back-in-stock email targets a different kind of missed sale than abandoned cart. An abandoned cart email reaches someone who added something to their basket and did not check out. A back-in-stock email reaches someone who found exactly what they wanted and discovered it was unavailable. Their intent is already established. Conversion rates on back-in-stock emails tend to run higher than most other automations for that reason.
The plugin here adds a shortcode you paste into any product page or template. Customers who enter their email are stored against that product. When you update the stock status to in stock in WooCommerce, the plugin sends each subscriber a plain-text email with a link to the product and removes them from the list. An admin table at WooCommerce > Back In Stock shows subscriber counts per product. Nothing leaves your server, and there is no subscription fee.
What one build gives you
- Sign-up form shortcode for product pages: customers enter their email and are queued for that product
- Automatic email to all subscribers when the product stock status is set to in stock
- Subscriber list cleared after sending so each address receives one notification per restock cycle
- Admin table at WooCommerce > Back In Stock showing subscriber count and most recent signup per product
What it does not do
- Variation-level waiting lists: tracks the parent product, not individual variations of a variable product
- Background sending: notifications fire synchronously in the admin save request, which is slow for large lists
- Double opt-in confirmation before adding a subscriber to the list
- Integration with external email platforms such as Klaviyo, Brevo or Mailchimp
- Subscriber export or per-subscriber management in the admin table
Back In Stock Notifier for WooCommerce does these. This covers the part most sites use.
The prompt
Loads into the composer so you can edit it first. Nothing is built, and nothing is charged, until you send it.
Build a WordPress plugin called WC Back In Stock. Plugin Name: WC Back In Stock. Version: 1.0.0. Description: Let customers sign up for back-in-stock emails on WooCommerce products, visible under WooCommerce > Back In Stock. Wrap all code in a class WC_Back_In_Stock with a public init() method. Instantiate the class and call init() on plugins_loaded. Database setup: Register register_activation_hook(__FILE__, ['WC_Back_In_Stock', 'create_table']). In the static method create_table: global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table = $wpdb->prefix . 'wc_back_in_stock'; $sql = "CREATE TABLE $table ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, product_id BIGINT UNSIGNED NOT NULL, email VARCHAR(200) NOT NULL, subscribed_at DATETIME NOT NULL, PRIMARY KEY (id), KEY product_id (product_id) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); Sign-up form: In init(), add_shortcode('wc_back_in_stock_form', [$this, 'render_form']). In render_form($atts, $content, $tag): $output = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['wc_bis_email']) && wp_verify_nonce($_POST['_wpnonce'] ?? '', 'wc_bis_signup')) { $product_id = get_the_ID(); $email = sanitize_email($_POST['wc_bis_email']); if (!is_email($email)) { $output .= '<p class="wc-bis-error">Please enter a valid email address.</p>'; } else { global $wpdb; $exists = $wpdb->get_var($wpdb->prepare("SELECT id FROM {$wpdb->prefix}wc_back_in_stock WHERE product_id = %d AND email = %s", $product_id, $email)); if ($exists) { $output .= '<p class="wc-bis-notice">You are already signed up for this product.</p>'; } else { $wpdb->insert($wpdb->prefix . 'wc_back_in_stock', ['product_id' => $product_id, 'email' => $email, 'subscribed_at' => current_time('mysql')], ['%d', '%s', '%s']); $output .= '<p class="wc-bis-success">We will email you when this product is back in stock.</p>'; } } } $output .= '<form method="post">'; $output .= wp_nonce_field('wc_bis_signup', '_wpnonce', true, false); $output .= '<p><input type="email" name="wc_bis_email" placeholder="Your email address" required></p>'; $output .= '<p><button type="submit">Notify me when back in stock</button></p>'; $output .= '</form>'; return $output; Stock notification: In init(), add_action('woocommerce_product_set_stock_status', [$this, 'maybe_notify'], 10, 3). In maybe_notify($product_id, $status, $product): if ($status !== 'instock') { return; } global $wpdb; $rows = $wpdb->get_results($wpdb->prepare("SELECT email FROM {$wpdb->prefix}wc_back_in_stock WHERE product_id = %d", $product_id)); if (empty($rows)) { return; } $name = $product->get_name(); $url = get_permalink($product_id); $subject = $name . ' is back in stock'; $body = $name . " is back in stock.\n\nShop now: " . $url; foreach ($rows as $row) { wp_mail($row->email, $subject, $body); } $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}wc_back_in_stock WHERE product_id = %d", $product_id)); Admin page: In init(), add_action('admin_menu', [$this, 'add_admin_page']). In add_admin_page(): add_submenu_page('woocommerce', 'Back In Stock', 'Back In Stock', 'manage_woocommerce', 'wc-back-in-stock', [$this, 'render_admin_page']). In render_admin_page(): global $wpdb; $rows = $wpdb->get_results("SELECT product_id, COUNT(*) as cnt, MAX(subscribed_at) as latest FROM {$wpdb->prefix}wc_back_in_stock GROUP BY product_id ORDER BY cnt DESC LIMIT 100"); echo '<div class="wrap"><h1>Back In Stock Subscribers</h1>'; if (empty($rows)) { echo '<p>No subscribers waiting.</p>'; } else { echo '<table class="wp-list-table widefat fixed striped"><thead><tr><th>Product</th><th>Subscribers</th><th>Latest signup</th></tr></thead><tbody>'; foreach ($rows as $row) { $title = get_the_title($row->product_id) ?: '(deleted)'; $link = get_permalink($row->product_id); echo '<tr><td><a href="' . esc_url($link) . '">' . esc_html($title) . '</a></td><td>' . intval($row->cnt) . '</td><td>' . esc_html($row->latest) . '</td></tr>'; } echo '</tbody></table>'; } echo '</div>'; No settings page, no stylesheet, no script.
What WooCommerce includes for stock management
WooCommerce handles stock tracking for the store owner. You can set quantities per product, enable low-stock warnings at a configurable threshold, and have stock update automatically when orders are placed. The product list in the admin shows current levels across your catalogue and flags anything that needs attention.
The customer-facing side covers display only. WooCommerce shows an out-of-stock badge and can hide unavailable products from shop archives if you prefer. It does not offer customers a way to register interest or receive a notification when a product returns.
The WooCommerce extension library includes a paid Waitlist plugin from WooCommerce itself, as well as third-party options such as Back In Stock Notifier for WooCommerce and YITH WooCommerce Waiting List. These add sign-up forms, subscriber management, and automated emails. The plugin in this guide covers the same core need for stores that want a single-file build without a recurring cost.
Why back-in-stock notifications are worth setting up early
Abandoned cart email requires session tracking, cart-level data, and a delay timer configured carefully enough that it does not fire on normal session drops. The customer also has to reach the cart before the sequence begins. For stores where products go out of stock regularly, back-in-stock reaches an earlier and more motivated group.
The trigger for back-in-stock is an event you control: updating the stock status. The subscriber list is self-selected; every address on it belongs to a customer who said they want that specific product. The email is short, factual, and expected. The sequence is deterministic, which makes testing it easier than any time-delayed automation.
The waiting list also gives you a lightweight demand signal. A product with forty subscribers is worth restocking before something with two. The admin table shows subscriber count per product and the date of the most recent signup, which is enough to prioritise a restock decision without additional tooling.
How the plugin works
The plugin registers a shortcode [wc_back_in_stock_form] that renders an email input and a submit button. You paste it into the product description, add it to a template file, or output it via a hook in your theme. The form submits to the same page and validates the email address before inserting a row into a custom database table. If the same email is already on the list for that product, the plugin shows a notice and does not create a duplicate.
When you save a product with its stock status set to in stock, the plugin intercepts the woocommerce_product_set_stock_status action. It queries the subscriber table for that product. If any subscribers exist, it sends each a plain-text email via wp_mail with the product name in the subject and a link to the product in the body, then deletes all subscriber records for that product. If the product goes out of stock again and new customers sign up, they will be notified on the next restock.
The admin page at WooCommerce > Back In Stock shows a grouped summary: product name as a link, subscriber count, and date of the most recent signup, ordered by count descending. There is no per-subscriber view, individual email lookup, or export in the initial build.
What the plugin does not cover
Variable products are tracked at the parent product level. If you sell a shirt in three sizes and only one size goes out of stock, customers who sign up via the parent product page are subscribed to the parent. Notification fires when the parent stock status changes to in stock, which may not match the moment a specific variation the customer wanted returns. Stores with variation-heavy catalogues need a dedicated plugin that tracks subscriptions at the variation level.
Notifications are sent synchronously in the same request that triggers the stock status change. For a product with a handful of subscribers this is fine. For a product with hundreds of waiting customers, the admin save request will be slow and could time out on shared hosting. If you expect large waiting lists, a plugin with queue-based email sending is the better tool.
There is no double opt-in. A customer who enters an email address is added immediately without a confirmation step. The follow-up prompt at the end of this guide adds a signed-token confirmation email if that matters for your legal environment or email list hygiene.
The plugin does not connect to Klaviyo, Brevo, Mailchimp, or any other email platform. All email goes through wp_mail using your site's configured mail transport. Without an SMTP plugin, deliverability will be poor and notifications may land in spam. Configure a transactional mail service before relying on this for customer-facing email.
When to use a dedicated plugin instead
If products regularly sell out and you expect waiting lists of more than a few dozen at a time, a plugin with background email sending is the right choice. Back In Stock Notifier for WooCommerce and YITH WooCommerce Waiting List both handle volume through queued sending rather than processing everything in the admin save request.
If you need variation-level waiting lists, subscriber export, or direct integration with your email marketing platform, those features do not exist in the initial build here. This plugin suits stores where out-of-stock events are occasional, lists stay small, and the goal is to capture demand without adding a recurring fee.
Paste the prompt below into Steem to build the plugin. To add double opt-in and an unsubscribe link, the follow-up is: 'Add double opt-in to WC Back In Stock: when the form is submitted, send a confirmation email with a signed URL containing a token. Only insert the subscriber row when the token link is clicked. Include an unsubscribe link in the stock notification email that removes the subscriber record via a second signed token URL.'
Questions
- Does the form appear automatically on out-of-stock product pages, or do I need to add it somewhere?
You add it manually using the shortcode [wc_back_in_stock_form]. Paste it into the product description, or output it in a template file with do_shortcode('[wc_back_in_stock_form]'). The shortcode renders regardless of stock status, so most stores place it using a conditional hook in the theme that checks stock status before outputting the shortcode.
- What happens if a customer enters their email twice for the same product?
The plugin checks for an existing record with the same product ID and email before inserting. If a duplicate is found it shows a notice and does not create a second row. The customer will still receive one notification when the product restocks.
- Will notifications fire if stock is updated via a CSV import or a third-party inventory plugin?
Yes, as long as the import or plugin routes through WooCommerce's standard data layer and triggers the woocommerce_product_set_stock_status action. Bulk imports that write directly to the database and bypass WooCommerce hooks will not fire notifications.
- Will the notification email actually arrive, or will it go to spam?
The plugin sends through wp_mail using your site's default mail configuration. Without a dedicated SMTP plugin, most shared hosting setups send from a server IP with no SPF or DKIM signing, and many emails land in spam or do not arrive. Configure WP Mail SMTP, FluentSMTP, or a similar plugin connected to a transactional mail service before relying on this for customer notifications.
- What happens to subscriber records if a product is permanently deleted?
The rows remain in the database table. The admin page shows the product title as '(deleted)' and the records persist until cleaned manually. A direct database query of DELETE FROM wp_wc_back_in_stock WHERE product_id NOT IN (SELECT ID FROM wp_posts) will remove orphaned rows.
The prompt
Loads into the composer so you can edit it first. Nothing is built, and nothing is charged, until you send it.
Build a WordPress plugin called WC Back In Stock. Plugin Name: WC Back In Stock. Version: 1.0.0. Description: Let customers sign up for back-in-stock emails on WooCommerce products, visible under WooCommerce > Back In Stock. Wrap all code in a class WC_Back_In_Stock with a public init() method. Instantiate the class and call init() on plugins_loaded. Database setup: Register register_activation_hook(__FILE__, ['WC_Back_In_Stock', 'create_table']). In the static method create_table: global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table = $wpdb->prefix . 'wc_back_in_stock'; $sql = "CREATE TABLE $table ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, product_id BIGINT UNSIGNED NOT NULL, email VARCHAR(200) NOT NULL, subscribed_at DATETIME NOT NULL, PRIMARY KEY (id), KEY product_id (product_id) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); Sign-up form: In init(), add_shortcode('wc_back_in_stock_form', [$this, 'render_form']). In render_form($atts, $content, $tag): $output = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['wc_bis_email']) && wp_verify_nonce($_POST['_wpnonce'] ?? '', 'wc_bis_signup')) { $product_id = get_the_ID(); $email = sanitize_email($_POST['wc_bis_email']); if (!is_email($email)) { $output .= '<p class="wc-bis-error">Please enter a valid email address.</p>'; } else { global $wpdb; $exists = $wpdb->get_var($wpdb->prepare("SELECT id FROM {$wpdb->prefix}wc_back_in_stock WHERE product_id = %d AND email = %s", $product_id, $email)); if ($exists) { $output .= '<p class="wc-bis-notice">You are already signed up for this product.</p>'; } else { $wpdb->insert($wpdb->prefix . 'wc_back_in_stock', ['product_id' => $product_id, 'email' => $email, 'subscribed_at' => current_time('mysql')], ['%d', '%s', '%s']); $output .= '<p class="wc-bis-success">We will email you when this product is back in stock.</p>'; } } } $output .= '<form method="post">'; $output .= wp_nonce_field('wc_bis_signup', '_wpnonce', true, false); $output .= '<p><input type="email" name="wc_bis_email" placeholder="Your email address" required></p>'; $output .= '<p><button type="submit">Notify me when back in stock</button></p>'; $output .= '</form>'; return $output; Stock notification: In init(), add_action('woocommerce_product_set_stock_status', [$this, 'maybe_notify'], 10, 3). In maybe_notify($product_id, $status, $product): if ($status !== 'instock') { return; } global $wpdb; $rows = $wpdb->get_results($wpdb->prepare("SELECT email FROM {$wpdb->prefix}wc_back_in_stock WHERE product_id = %d", $product_id)); if (empty($rows)) { return; } $name = $product->get_name(); $url = get_permalink($product_id); $subject = $name . ' is back in stock'; $body = $name . " is back in stock.\n\nShop now: " . $url; foreach ($rows as $row) { wp_mail($row->email, $subject, $body); } $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}wc_back_in_stock WHERE product_id = %d", $product_id)); Admin page: In init(), add_action('admin_menu', [$this, 'add_admin_page']). In add_admin_page(): add_submenu_page('woocommerce', 'Back In Stock', 'Back In Stock', 'manage_woocommerce', 'wc-back-in-stock', [$this, 'render_admin_page']). In render_admin_page(): global $wpdb; $rows = $wpdb->get_results("SELECT product_id, COUNT(*) as cnt, MAX(subscribed_at) as latest FROM {$wpdb->prefix}wc_back_in_stock GROUP BY product_id ORDER BY cnt DESC LIMIT 100"); echo '<div class="wrap"><h1>Back In Stock Subscribers</h1>'; if (empty($rows)) { echo '<p>No subscribers waiting.</p>'; } else { echo '<table class="wp-list-table widefat fixed striped"><thead><tr><th>Product</th><th>Subscribers</th><th>Latest signup</th></tr></thead><tbody>'; foreach ($rows as $row) { $title = get_the_title($row->product_id) ?: '(deleted)'; $link = get_permalink($row->product_id); echo '<tr><td><a href="' . esc_url($link) . '">' . esc_html($title) . '</a></td><td>' . intval($row->cnt) . '</td><td>' . esc_html($row->latest) . '</td></tr>'; } echo '</tbody></table>'; } echo '</div>'; No settings page, no stylesheet, no script.
Steem