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.
Table of Contents
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:
- Customer hits an out-of-stock product, enters their email in a form
- Their email gets stored in a custom table with ‘pending’ status
- Verification email goes out with a unique token link
- They click the link, status changes to ‘subscribed’, follow-up emails get scheduled
- When you update inventory, WooCommerce fires a hook we’re listening for
- System queries all subscribed emails for that product, sends notifications
- If the product’s still in stock after 24 and 72 hours, reminder emails fire
- 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:
- Verify nonce
- Sanitize and validate the email (use WordPress’s built-in
sanitize_email()andis_email()) - Generate verification token using
bin2hex(random_bytes(32)) - Insert subscription with ‘pending’ status
- 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:
- When saving subscription, generate a unique token
- Send verification email with a link:
https://yoursite.com/?action=verify_stock_notification&token=abc123... - Hook into
template_redirectto catch verification clicks - Look up the token, update status from ‘pending’ to ‘subscribed’
- 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:
- Query subscribers with ‘subscribed’ status for this product/variation
- Loop through each subscriber
- Send immediate back-in-stock notification
- Schedule follow-up reminder emails (24 and 72 hours)
- 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.phpevery 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:
- Get order via
wc_get_order( $order_id ) - Extract email
$order->get_billing_email() - Loop through
$order->get_items() - 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.