Restock monitors are the backbone of any serious restocking strategy. They watch product pages for inventory changes and alert you the moment something becomes available. While there are excellent pre-built tools covered in our restock monitor tools guide, building your own gives you full control over what you monitor, how often you check, and how you receive alerts. This tutorial walks you through building a functional restock monitor using Python, from basic web scraping concepts to a complete working system that sends alerts to your Discord server.

What You Will Build

By the end of this guide, you will have a Python script that:

  1. Checks a product page at regular intervals.
  2. Detects when the product status changes from “out of stock” to “in stock.”
  3. Sends an alert to a Discord channel via webhook with the product name, price, and link.
  4. Logs all checks and status changes for debugging.
  5. Runs continuously on a schedule without manual intervention.

This is a learning project. The monitor we build is functional and useful, but it is intentionally simple. Production-grade monitors used by large restock communities are significantly more complex, handling hundreds of products across dozens of retailers with proxy rotation, session management, and distributed architecture. Our goal is to understand the fundamentals so you can extend the system as your skills grow.

Prerequisites

You need the following before starting:

Technical Requirements

  • Python 3.8 or newer installed on your computer
  • pip (Python’s package manager, included with Python)
  • A text editor or IDE (VS Code, PyCharm, or even Notepad work fine)
  • Basic command line familiarity (running commands in Terminal or Command Prompt)

Accounts and Services

  • A Discord server where you have permission to create webhooks
  • A target product page you want to monitor (we will use a public retailer page as our example)

Python Libraries

We will use these libraries, all installable via pip:

LibraryPurpose
requestsSending HTTP requests to fetch web pages
beautifulsoup4Parsing HTML to extract product information
scheduleRunning our check function at regular intervals
datetimeTimestamping log entries and alerts

Install them with a single command:

pip install requests beautifulsoup4 schedule

Understanding Web Scraping Basics

Before writing code, you need to understand what happens when you visit a product page in your browser and how we replicate that process in Python.

How Web Pages Work

When you type a URL into your browser:

  1. Your browser sends an HTTP GET request to the server.
  2. The server responds with HTML (the page structure), CSS (styling), and JavaScript (interactivity).
  3. Your browser renders this response into the visual page you see.

Our Python script does step 1 and receives the response from step 2, but it does not render the page visually. Instead, we parse the HTML to find the specific information we need — typically whether an “Add to Cart” button exists or whether a “Sold Out” message is present.

What We Look For

Product availability is usually indicated by one of these HTML patterns:

  • An “Add to Cart” button that appears when the product is in stock and disappears (or becomes disabled) when it is not.
  • A text element displaying “In Stock,” “Out of Stock,” “Sold Out,” or similar status messages.
  • A price element that only appears when the product is available.
  • A CSS class or data attribute on the product container that changes based on availability (e.g., class="product-available" vs class="product-unavailable").

The exact pattern depends on the retailer. You will need to inspect the target page’s HTML to find the right element to monitor.

Inspecting a Product Page

To find the HTML element that indicates stock status:

  1. Open the product page in your browser.
  2. Right-click on the “Add to Cart” button or the stock status text.
  3. Select “Inspect” or “Inspect Element.”
  4. Note the HTML element’s tag (e.g., button, span, div), its class names, its ID, and the text it contains.
  5. This information is what you will tell BeautifulSoup to search for.

For example, you might find something like:

<button class="add-to-cart-btn" data-product-id="12345">Add to Cart</button>

Or when sold out:

<span class="product-status sold-out">Currently Unavailable</span>

Building the Monitor Step by Step

Step 1: Project Structure

Create a project folder with the following files:

restock-monitor/
    monitor.py        # Main monitoring script
    config.py         # Configuration (URLs, webhook, timing)
    notifier.py       # Discord notification logic

Step 2: Configuration File

The configuration file stores all the settings your monitor needs. Keeping configuration separate from logic makes it easy to change what you monitor without touching the core code.

# config.py

# Product to monitor
PRODUCT_URL = "https://www.example-retailer.com/product/12345"
PRODUCT_NAME = "Example Product Name"

# How often to check (in seconds)
CHECK_INTERVAL = 30

# Discord webhook URL (from your server settings)
DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN"

# Request settings
HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
}

# What to look for on the page (customize per retailer)
IN_STOCK_INDICATOR = "Add to Cart"
OUT_OF_STOCK_INDICATOR = "Sold Out"

The HEADERS dictionary is important. Without a realistic User-Agent header, many retailers will block your requests because they look like they are coming from a script rather than a real browser. This does not make our request deceptive — it simply ensures the server sends us the standard HTML response rather than a bot-block page.

Step 3: Discord Notification Module

Discord webhooks are the easiest way to receive instant alerts on your phone. Setting one up takes 30 seconds, and sending a notification requires nothing more than an HTTP POST request.

Creating a Discord Webhook

  1. Open your Discord server.
  2. Go to Server Settings, then Integrations, then Webhooks.
  3. Click “New Webhook.”
  4. Name it (e.g., “Restock Alerts”).
  5. Select the channel where alerts should be posted.
  6. Copy the webhook URL and paste it into your config.py.

The Notifier Code

# notifier.py

import requests
from datetime import datetime


def send_discord_alert(webhook_url, product_name, product_url, status, price=None):
    """Send a restock alert to Discord via webhook."""

    if status == "in_stock":
        color = 3066993  # Green
        title = f"IN STOCK: {product_name}"
        description = f"**{product_name}** is now available!"
    else:
        color = 15158332  # Red
        title = f"Out of Stock: {product_name}"
        description = f"**{product_name}** is no longer available."

    embed = {
        "title": title,
        "description": description,
        "url": product_url,
        "color": color,
        "fields": [],
        "footer": {
            "text": f"Restock Monitor | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
        },
    }

    if price:
        embed["fields"].append({"name": "Price", "value": price, "inline": True})

    embed["fields"].append(
        {"name": "Link", "value": f"[Buy Now]({product_url})", "inline": True}
    )

    payload = {"embeds": [embed]}

    try:
        response = requests.post(webhook_url, json=payload)
        if response.status_code == 204:
            print(f"[{datetime.now()}] Discord alert sent successfully.")
        else:
            print(
                f"[{datetime.now()}] Discord alert failed: {response.status_code}"
            )
    except Exception as e:
        print(f"[{datetime.now()}] Error sending Discord alert: {e}")

This sends a rich embed message to your Discord channel with color coding (green for in stock, red for out of stock), the product name and link, the price if available, and a timestamp.

If you already use Discord for restock alerts from community servers, this custom webhook will integrate seamlessly alongside those notifications. Our guide on Discord servers for restock alerts lists the best communities to pair with your personal monitor.

Step 4: The Main Monitor Script

This is the core of the system. It fetches the product page, parses the HTML to determine stock status, compares it to the previous status, and sends a notification if it changed.

# monitor.py

import requests
from bs4 import BeautifulSoup
from datetime import datetime
import time
import schedule

from config import (
    PRODUCT_URL,
    PRODUCT_NAME,
    CHECK_INTERVAL,
    DISCORD_WEBHOOK_URL,
    HEADERS,
    IN_STOCK_INDICATOR,
    OUT_OF_STOCK_INDICATOR,
)
from notifier import send_discord_alert


# Track the last known status
last_status = None


def check_product():
    """Fetch the product page and check stock status."""
    global last_status

    try:
        response = requests.get(PRODUCT_URL, headers=HEADERS, timeout=15)
        response.raise_for_status()

        soup = BeautifulSoup(response.text, "html.parser")
        page_text = soup.get_text().lower()

        # Determine current status
        if IN_STOCK_INDICATOR.lower() in page_text:
            current_status = "in_stock"
        elif OUT_OF_STOCK_INDICATOR.lower() in page_text:
            current_status = "out_of_stock"
        else:
            current_status = "unknown"
            print(
                f"[{datetime.now()}] WARNING: Could not determine status for "
                f"{PRODUCT_NAME}"
            )
            return

        # Extract price if possible
        price = extract_price(soup)

        # Log the check
        print(
            f"[{datetime.now()}] Checked {PRODUCT_NAME}: {current_status}"
            + (f" | Price: {price}" if price else "")
        )

        # If status changed, send alert
        if last_status is not None and current_status != last_status:
            print(
                f"[{datetime.now()}] STATUS CHANGE DETECTED: "
                f"{last_status} -> {current_status}"
            )
            send_discord_alert(
                DISCORD_WEBHOOK_URL,
                PRODUCT_NAME,
                PRODUCT_URL,
                current_status,
                price,
            )

        last_status = current_status

    except requests.exceptions.Timeout:
        print(f"[{datetime.now()}] Request timed out for {PRODUCT_NAME}")
    except requests.exceptions.HTTPError as e:
        print(f"[{datetime.now()}] HTTP error for {PRODUCT_NAME}: {e}")
    except Exception as e:
        print(f"[{datetime.now()}] Error checking {PRODUCT_NAME}: {e}")


def extract_price(soup):
    """Attempt to extract the product price from the page."""
    # Common price element patterns -- customize for your target retailer
    price_selectors = [
        {"class": "price"},
        {"class": "product-price"},
        {"class": "sale-price"},
        {"itemprop": "price"},
    ]

    for selector in price_selectors:
        element = soup.find(attrs=selector)
        if element:
            return element.get_text(strip=True)

    return None


def main():
    """Start the monitor."""
    print(f"{'=' * 50}")
    print(f"Restock Monitor Started")
    print(f"Product: {PRODUCT_NAME}")
    print(f"URL: {PRODUCT_URL}")
    print(f"Check Interval: {CHECK_INTERVAL} seconds")
    print(f"{'=' * 50}")

    # Run the first check immediately
    check_product()

    # Schedule subsequent checks
    schedule.every(CHECK_INTERVAL).seconds.do(check_product)

    while True:
        schedule.run_pending()
        time.sleep(1)


if __name__ == "__main__":
    main()

Step 5: Running the Monitor

Open a terminal, navigate to your project folder, and run:

python monitor.py

You should see output like:

==================================================
Restock Monitor Started
Product: Example Product Name
URL: https://www.example-retailer.com/product/12345
Check Interval: 30 seconds
==================================================
[2026-02-08 10:00:00] Checked Example Product Name: out_of_stock
[2026-02-08 10:00:30] Checked Example Product Name: out_of_stock
[2026-02-08 10:01:00] Checked Example Product Name: in_stock | Price: $499.99
[2026-02-08 10:01:00] STATUS CHANGE DETECTED: out_of_stock -> in_stock
[2026-02-08 10:01:00] Discord alert sent successfully.

Customizing for Specific Retailers

The basic monitor works by searching for text on the page. For more reliable detection, you should customize the stock-checking logic for each specific retailer.

Using CSS Selectors

Instead of searching the entire page text, target specific elements:

# Check for a specific button by class name
add_to_cart = soup.find("button", class_="add-to-cart-btn")
if add_to_cart and not add_to_cart.get("disabled"):
    current_status = "in_stock"

Using Data Attributes

Some retailers use data attributes to indicate availability:

# Check a data attribute on the product container
product_div = soup.find("div", {"data-product-id": "12345"})
if product_div and product_div.get("data-available") == "true":
    current_status = "in_stock"

Handling JavaScript-Rendered Pages

Some retailer pages load stock status via JavaScript after the initial HTML loads. The requests library only fetches the initial HTML and does not execute JavaScript. For these pages, you need a browser automation tool.

A lightweight option is requests-html, which includes a minimal JavaScript renderer:

from requests_html import HTMLSession

session = HTMLSession()
response = session.get(PRODUCT_URL, headers=HEADERS)
response.html.render(timeout=20)  # Execute JavaScript
soup = BeautifulSoup(response.html.html, "html.parser")

For more complex JavaScript-heavy pages, you may need Selenium or Playwright, though these are heavier tools with higher resource requirements.

Monitoring Multiple Products

To monitor more than one product, restructure your configuration as a list:

# config.py

PRODUCTS = [
    {
        "url": "https://www.retailer.com/product/1",
        "name": "Product One",
        "in_stock_text": "Add to Cart",
        "out_of_stock_text": "Sold Out",
    },
    {
        "url": "https://www.retailer.com/product/2",
        "name": "Product Two",
        "in_stock_text": "Buy Now",
        "out_of_stock_text": "Currently Unavailable",
    },
]

Then update your monitor to iterate over the list:

# Track status per product
product_statuses = {}

def check_all_products():
    for product in PRODUCTS:
        check_product(product)
        time.sleep(2)  # Small delay between requests to the same retailer

Adding a small delay between requests to the same retailer is important. Hitting a server with rapid consecutive requests can trigger rate limiting and get your IP temporarily blocked.

Setting Up Scheduling

Running on Your Computer

The simplest approach is to keep a terminal window open with python monitor.py running. This works but stops when you close the terminal or your computer goes to sleep.

Running as a Background Service

On Linux or macOS, use nohup or screen:

nohup python monitor.py &

On Windows, you can use Task Scheduler to run the script at system startup.

Running on a Server or Cloud

For 24/7 monitoring, consider running your script on:

  • A Raspberry Pi — Low-cost, always-on hardware perfect for running lightweight scripts.
  • A VPS (Virtual Private Server) — Services like DigitalOcean ($5/month), Linode, or Vultr provide always-on Linux servers.
  • PythonAnywhere — Free tier includes scheduled tasks, though with limitations on external HTTP requests.
  • A home server — Any old computer running Linux can serve as a dedicated monitor host.

Ethical Considerations

Building your own monitor raises ethical questions that are worth addressing head-on.

What Is Acceptable

  • Monitoring at reasonable intervals (every 15 to 60 seconds). This places minimal load on the retailer’s servers, comparable to a single user refreshing a page occasionally.
  • Monitoring for personal purchasing. You are tracking availability so you can manually buy the product yourself.
  • Monitoring public pages. Product pages are intended to be viewed by the public.

What Crosses the Line

  • Aggressive request rates (every 1 to 2 seconds or faster). This can burden servers and may constitute a denial-of-service if scaled up.
  • Automating the purchase itself. Our monitor only alerts you. Adding automatic add-to-cart or checkout functionality crosses into bot territory, which violates retailer terms of service and potentially the law.
  • Scraping for resale intelligence. Building monitors to identify profitable flip opportunities at scale contributes to the scalping problem that makes restocking necessary in the first place. Our analysis of restocking vs reselling explores this distinction in depth.
  • Circumventing access controls. If a page requires login, CAPTCHA, or other access controls, bypassing them programmatically is both unethical and likely illegal.

Respecting robots.txt

Every website has a robots.txt file (accessible at https://example.com/robots.txt) that specifies which pages automated tools are and are not allowed to access. While robots.txt is not legally binding in all jurisdictions, respecting it is a matter of good practice.

Check your target retailer’s robots.txt before monitoring. If product pages are disallowed for bots, consider using a browser extension like Distill instead, which operates within your browser and does not send external automated requests.

Extending Your Monitor

Once the basic system works, here are useful additions:

Price Tracking

Store price history in a simple JSON file or SQLite database. This lets you identify price drops, set price-based alerts, and track trends over time.

Multi-Channel Notifications

In addition to Discord, you can add:

  • Email alerts using Python’s built-in smtplib
  • SMS alerts using Twilio’s API (free tier includes limited messages)
  • Push notifications using services like Pushover or Pushbullet

Error Recovery

Add retry logic so your monitor does not stop at the first error:

import time

MAX_RETRIES = 3

def check_with_retry(product):
    for attempt in range(MAX_RETRIES):
        try:
            check_product(product)
            return
        except Exception as e:
            print(f"Attempt {attempt + 1} failed: {e}")
            if attempt < MAX_RETRIES - 1:
                time.sleep(5)

Logging to File

Replace print statements with Python’s logging module for persistent, searchable logs:

import logging

logging.basicConfig(
    filename="monitor.log",
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
)

logging.info(f"Checked {product_name}: {status}")

Troubleshooting Common Issues

ProblemLikely CauseSolution
Request returns 403 ForbiddenMissing or blocked User-AgentUpdate the User-Agent header in your config
HTML does not contain expected elementsPage uses JavaScript renderingSwitch to requests-html or Selenium
Discord webhook returns 429Sending too many messagesAdd rate limiting (max 1 message per 5 seconds)
Monitor reports false positivesStock indicator text appears in other page elementsUse more specific CSS selectors instead of full-page text search
IP gets temporarily blockedCheck interval too aggressiveIncrease the interval to 30-60 seconds minimum
Different HTML on each requestRetailer A/B testing or CDN variationsTarget the most stable elements (IDs over classes)

FAQ

Do I need programming experience to build a restock monitor?

Basic programming familiarity helps, but this tutorial is designed for beginners. If you can install Python, run commands in a terminal, and edit text files, you can follow along. The code examples are complete and explained step by step. If you prefer not to code at all, our restock monitor tools guide covers pre-built solutions that require no programming.

The legality of web scraping depends on jurisdiction and context. In the US, the Ninth Circuit ruled in hiQ Labs v. LinkedIn (2022) that scraping publicly accessible data does not violate the Computer Fraud and Abuse Act. However, violating a website’s terms of service could create civil liability. Monitoring a public product page at reasonable intervals for personal use is generally considered low risk, but you should review the specific retailer’s terms of service and robots.txt file.

How often should I check a product page?

For personal monitoring, checking every 15 to 60 seconds provides a good balance between speed and server respect. Intervals shorter than 10 seconds are unnecessarily aggressive for a single user and increase your risk of being rate-limited or blocked. If you are monitoring multiple products on the same retailer, add delays between requests so you are not hitting their server multiple times per second.

Can my restock monitor automatically buy the product for me?

Technically possible, but strongly discouraged. Automating the purchase process makes your tool a bot, which violates virtually every retailer’s terms of service and may violate laws like the BOTS Act. The monitor we build in this guide only notifies you. You then manually visit the page and complete the purchase yourself. This is the ethical and legal boundary.

Why does my monitor work on some sites but not others?

Different websites use different technologies to render their pages. Sites that load content primarily through HTML (server-side rendering) work well with the basic requests + BeautifulSoup approach. Sites that load stock status via JavaScript (client-side rendering) require a tool that can execute JavaScript, like Selenium, Playwright, or requests-html. Inspect the page source (not the rendered page) to determine which approach you need.