Add back-in-stock email notifications to WooCommerce
Published Updated 3 min read
WooCommerce emails the store admin when stock runs low and offers the customer nothing. A visitor on an out-of-stock product page sees the status and that is all: no sign-up field, no waiting list, no email when you restock. They check back, or they leave.
That visitor is worth more than most. An abandoned-cart email chases someone who hesitated at the basket; a back-in-stock email reaches someone who found exactly what they wanted and could not have it. The intent is already there, which is why these emails tend to convert better than most automations.
The plugin adds a shortcode for the sign-up form, stores each address against the product, and emails everyone on the list when you set the product back in stock. Nothing leaves your server and there is no subscription fee.
Stock tools for you, a badge for the customer
WooCommerce's stock management is built for the store owner: quantities per product, low-stock warnings at a threshold you set, and stock that updates itself as orders are placed. On the customer side it only displays. There is an out-of-stock badge, and you can hide unavailable products from shop archives.
Waiting lists come from extensions: WooCommerce's own paid Waitlist, Back In Stock Notifier for WooCommerce, and YITH WooCommerce Waiting List. They add the form, subscriber management and the emails. For a store that wants the core of that in a single file without a recurring cost, this build covers it.
A waiting list you can read as demand
The trigger is an event you control, setting the stock status, and every address on the list belongs to someone who asked about that product. The email is short and expected, and because nothing runs on a timer it is easier to test than any delayed automation.
The list doubles as a demand signal. A product with forty people waiting is worth restocking before one with two. The admin page at WooCommerce > Back In Stock shows each product with its subscriber count and the date of the latest signup, busiest first, which is enough to prioritise a restock without other tooling.
From sign-up to email
The shortcode [wc_back_in_stock_form] renders an email field and a button. The form submits to the same page, validates the address and inserts a row into a custom table. The same address on the same product gets a notice instead of a duplicate row.
When a product is saved as in stock, the plugin catches woocommerce_product_set_stock_status, sends each subscriber a plain-text email through wp_mail with the product name in the subject and a link in the body, then deletes that product's subscriber rows. Anyone who signs up after the next sell-out hears about the restock after that.
The form renders whatever the stock status, so most stores output it from a theme hook that checks stock first, with do_shortcode('[wc_back_in_stock_form]'), rather than pasting it into every product description.
Where it strains: variations, long lists and mail delivery
Subscriptions attach to the parent product. Sell a shirt in three sizes, run out of one, and a customer who signs up is waiting on the parent, which can come back in stock before the size they wanted does.
Emails go out inside the same request that saves the product. A handful of subscribers is fine; hundreds make the save slow and can time it out on shared hosting. If you expect long lists, Back In Stock Notifier and YITH both queue their sending, and that alone is worth paying for.
Mail goes through your site's own transport. On most shared hosting that means a server IP with no SPF or DKIM signing, and a good share of notifications landing in spam. Connect WP Mail SMTP, FluentSMTP or similar to a transactional mail service before customers rely on it. A follow-up message can add double opt-in and an unsubscribe link, each through a signed token URL.
What you get
Included
- 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
Not included
- 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 the things on the right. This build covers the part most sites use.
Questions
Will notifications fire if stock is updated by a CSV import or an inventory plugin?
Yes, as long as the import or plugin goes through WooCommerce's standard data layer and triggers woocommerce_product_set_stock_status. Bulk imports that write straight to the database and bypass WooCommerce hooks will not send anything.
What happens to subscribers when a product is permanently deleted?
Their rows stay in the table, and the admin page lists the product as '(deleted)'. DELETE FROM wp_wc_back_in_stock WHERE product_id NOT IN (SELECT ID FROM wp_posts) removes the orphans.
Build it in Steem
The button opens the builder with this guide's prompt filled in, so you can change anything before it runs. Nothing is built, and nothing is charged, until you send it.
Read the prompt (457 words)
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