You’re sitting there wondering why your traffic doesn’t convert, meanwhile half your visitors are hitting dead ends on products they actually want to buy.

Baymard’s research says 9% of abandoned carts happen because items are unavailable. That’s not even counting the people who bounce before adding anything to cart. They see “Out of Stock,” shrug, and leave. Maybe they bookmark it to check later. Spoiler: they don’t check later.

What you need is a back-in-stock notification system. Takes about 2-3 hours to implement if you know what you’re doing. After 13 years of client work, this is the approach that actually holds up in production.

System Architecture Overview

Let me walk you through the flow before we start coding. Understanding the architecture up front saves you from those debugging sessions wondering why emails aren’t sending:

  1. Customer hits an out-of-stock product, enters their email in a form
  2. Their email gets stored in a custom table with ‘pending’ status
  3. Verification email goes out with a unique token link
  4. They click the link, status changes to ‘subscribed’, follow-up emails get scheduled
  5. When you update inventory, WooCommerce fires a hook we’re listening for
  6. System queries all subscribed emails for that product, sends notifications
  7. If the product’s still in stock after 24 and 72 hours, reminder emails fire
  8. Customer completes checkout, their subscription flips to ‘completed’

Here’s the thing about the database architecture – I used to use post meta for this stuff years ago. Mistake.

When you’ve got a popular product with 500+ subscribers and you restock it, you’re doing 500 individual post meta queries. Your server chokes.

Custom table with proper indexes? Instant lookups, no matter how many subscribers you have.

Database Table Setup

You need a dedicated table. Here’s the structure I use across probably 20+ client sites at this point:

  • email – Subscriber’s email address
  • product_id – Parent product ID
  • variation_id – Specific variation (0 for simple products)
  • status – ‘pending’, ‘subscribed’, ‘completed’, or ‘cancelled’
  • verification_token – Unique hash for email verification
  • subscribed_date – Timestamp when they signed up

The critical part everyone screws up: indexes. You need indexes on product_id, email, and status. Without them, your queries crawl as you get more data. With 50 subscribers, you won’t notice. With 5,000 subscribers, you’ll notice.

Here’s why: when you restock a product, you’re running a query like “find all emails with status=’subscribed’ for product_id=123”.

No index?

Database scans every single row. With an index? Goes straight to the relevant rows. Night and day difference on performance.

Frontend Form Implementation

WooCommerce gives you two hooks for this – one for simple products, one for variable:

add_action( 'woocommerce_simple_add_to_cart', 'append_notify_form', 35 );
add_action( 'woocommerce_variable_add_to_cart', 'append_notify_form', 35 );

Check $product->is_in_stock() before displaying anything – no point showing a notification form for in-stock products.

Your form needs these fields:

  • Email input
  • Hidden product ID
  • Hidden variation ID (for variable products)
  • Submit button that triggers AJAX
  • Nonce for security

For variable products, here’s the trick: WooCommerce already updates a hidden variation_id field when customers select product options. Hook into that field with your JavaScript.

Don’t reinvent the wheel by trying to track variations yourself.

AJAX Submission Handler

Register AJAX handlers:

add_action( 'wp_ajax_save_notify_email', 'save_notify_email' );
add_action( 'wp_ajax_nopriv_save_notify_email', 'save_notify_email' );

Your handler logic:

  1. Verify nonce
  2. Sanitize and validate the email (use WordPress’s built-in sanitize_email() and is_email())
  3. Generate verification token using bin2hex(random_bytes(32))
  4. Insert subscription with ‘pending’ status
  5. Trigger verification email

Note: Checks for existing subscriptions first which avoids unnecessary emails sent.

Email Verification Flow

Don’t skip verification.

Without verification, anyone can subscribe someone else’s email address. Happens all the time – competitors, pranks, people typoing their own email. You start sending notifications to addresses that never opted in, they mark you as spam, your domain reputation craters, and suddenly legitimate emails aren’t making it to anyone’s inbox.

I’ve consulted on three different stores where this happened. Each time, it took months to rebuild their email reputation. Not worth it.

Here’s the flow:

  1. When saving subscription, generate a unique token
  2. Send verification email with a link: https://yoursite.com/?action=verify_stock_notification&token=abc123...
  3. Hook into template_redirect to catch verification clicks
  4. Look up the token, update status from ‘pending’ to ‘subscribed’
  5. Redirect to a success page

In your template_redirect hook, check if $_GET['action'] equals ‘verify_stock_notification’. If yes, grab the token, query your database, update the status, and redirect with a confirmation message.

Legal angle: GDPR requires explicit consent for marketing emails. The verification link proves consent. Without it, you’re technically in violation. Will you get caught? Maybe not. But why risk it?

Stock Change Detection and Send Emails

WooCommerce gives you specific hooks for stock changes:

add_action( 'woocommerce_product_set_stock', 'notify_subscribers', 10, 1 );
add_action( 'woocommerce_variation_set_stock', 'notify_subscribers', 10, 1 );

These hooks fire exactly when stock levels change – nothing else. Clean, efficient, and you get the $product object passed directly.

Your handler needs to check the stock status first:

function notify_subscribers( $product ) {
    $stock_status = $product->get_stock_status();
    if ( 'outofstock' === $stock_status ) return;
    $product_id = $product->get_id();
    $product_type = $product->get_type();
    $parent_id = ( 'variation' === $product_type ) ? $product->get_parent_id() : 0;
    // Query subscribers and send notifications
}

When you confirm stock is available, here’s your process:

  1. Query subscribers with ‘subscribed’ status for this product/variation
  2. Loop through each subscriber
  3. Send immediate back-in-stock notification
  4. Schedule follow-up reminder emails (24 and 72 hours)
  5. Update any tracking you need

For the follow-up emails, schedule them using wp_schedule_single_event():

wp_schedule_single_event(
    time() + DAY_IN_SECONDS,
    'send_stock_followup_email',
    array( $subscriber_id, $product_id, 1 )
);

wp_schedule_single_event(
    time() + ( 3 * DAY_IN_SECONDS ),
    'send_stock_followup_email',
    array( $subscriber_id, $product_id, 2 )
);

Then register your callback:

add_action( 'send_stock_followup_email', 'handle_followup_email', 10, 3 );

function handle_followup_email( $subscriber_id, $product_id, $sequence ) {
    // Get subscriber, check status and stock, send reminder if valid
}

Tip: WP-Cron only runs when someone visits your site. On low-traffic stores, those reminders might not fire exactly on schedule. Set up a real server-level cron to hit wp-cron.php every 15 minutes.

Coupon Generation

Want better conversion? Generate unique coupons for each notification using WC_Coupon:

function generate_discount_coupon( $product, $subscriber_email ) {
    $code = 'STOCK-' . strtoupper( substr( md5( $subscriber_email . time() ), 0, 8 ) );
    $coupon = new WC_Coupon();
    $coupon->set_code( $code );
    $coupon->set_discount_type( 'percent' );
    $coupon->set_amount( 10 );
    $coupon->set_date_expires( strtotime( '+7 days' ) );
    $coupon->set_product_ids( array( $product->get_id() ) );
    $coupon->set_usage_limit( 1 );
    $coupon->set_usage_limit_per_user( 1 );
    $coupon->set_individual_use( true );
    $coupon->save();
    return $code;
}

Each coupon:

  • Works for 7 days
  • Applies only to the specific product
  • Single-use only
  • Can’t be stacked

Purchase Completion Tracking

add_action( 'woocommerce_order_status_completed', 'mark_subscription_completed' );

This fires when orders hit ‘completed’ status. Your logic:

  1. Get order via wc_get_order( $order_id )
  2. Extract email $order->get_billing_email()
  3. Loop through $order->get_items()
  4. Update database to ‘completed’

Once purchased, stop sending emails for that product. Clean old records every 90 days.

Final Thought

  • Watch subscription rates: If under 5%, fix form visibility or messaging.
  • Test coupon amounts: Start at 10%, experiment.
  • Monitor conversions: Use UTM parameters for tracking.
  • Email deliverability: Use SMTP (SendGrid, SES) instead of mail().
  • Add unsubscribe links: Required legally, prevents spam complaints.

Masood

Helping WooCommerce Stores Increase Sales & Revenue with Smart Plugins & WordPress Solutions