Data Tooly logo Data Tooly
Guide · E-commerce Intelligence

Back in Stock Alert Scraper: Products, Sizes, Colours

Build back in stock alerts with a scraper: the stock fields to read, size and colour restocks from variants, null vs out of stock, check cadence, and cost.

Published · Updated · By Omar Eldeeb

A back-in-stock alert is a scheduled check of product pages that compares each item's stock flag, and each size's or colour's, with the last check, and alerts when one flips from out of stock to in stock. Read inStock (true, false or unknown) per product and per variant, ignore unknowns, run hourly, and alert on the flip.

This guide builds that with E-commerce Product Scraper Pro on Apify, using real rows from test runs on 17-20 October 2026, including one real size restock on Zalando.

The stock fields you get

Every product row has three stock fields, and every entry in variants[] has its own:

Field Meaning Real values seen
inStock Normalised flag: true, false, or null (unknown) true, false, null
availability The stock state as the page gives it, usually schema.org's term InStock, OutOfStock, PreOrder, BackOrder, LimitedAvailability, or the site's own text
stockQty A count read from text like "Only 3 left" 1 on amazon.com's Anker USB-C cable (LimitedAvailability)
variants[].inStock, variants[].availability The same, per size / colour / pack, with the variant's sku, price and options {"Size": "43"}, {"Color": "Damson Blue"}

Across 10,645 product rows from our October test runs, inStock was true 8,344 times, false 406 times (387 OutOfStock, 13 PreOrder, 6 BackOrder) and null 1,895 times. Note that pre-order and back-order count as not in stock.

null matters most. On amazon.de, 50 rows came back with inStock: null and an availability of "Dieser Artikel kann nicht an den von dir ausgewählten Zustellungsort versendet werden" (Amazon saying it can't deliver to the selected location). That says nothing about stock. If your alert logic treats null as "out", the next normal page looks like a restock. Only a known false followed by a known true is a restock.

Variant-level stock: sizes and colours

includeVariants is on by default. Here are two variants of a real John Lewis row from 2026-10-18 (trimmed); two of its five colours were out of stock while the product as a whole was in stock:

json
{
  "name": "Sage Barista Express Impress Bean to Cup Coffee Machine",
  "inStock": true,
  "availability": "InStock",
  "variants": [
    { "sku": "241219588", "price": 599.95, "inStock": true, "availability": "InStock", "options": { "Color": "Brushed Stainless Steel" } },
    { "sku": "110092884", "price": 599.95, "inStock": false, "availability": "OutOfStock", "options": { "Color": "Damson Blue" } }
  ]
}

Most "back in stock" demand is like this: one colour or size, not the product. So watch the variants.

Option 1: let monitor mode do the diff

Run the actor with monitorMode: true and onlyChanges: true on a schedule. It stores every product and variant between runs and writes only what changed:

  • When the whole product flips from out to in stock, changeType is back-in-stock.
  • When one variant flips, the product's changeType is changed, changes contains variant-stock-changed, and the variant appears in changedVariants with previousInStock and inStock.
  • A stock count drifting (12 left, then 11) is not reported as a change, so limited-stock items don't alert every run.

This reads one monitor run and prints restock alerts at both levels:

python
"""Restock alerts from one monitor-mode run (product level and size/colour level)."""
import os
import sys

from apify_client import ApifyClient


def restock_alerts(rows):
    for r in rows:
        if r.get("recordType") != "product":
            continue
        if r.get("changeType") == "back-in-stock":                   # the whole product came back
            yield f"{r['name']} is back in stock at {r['price']} {r['currency']}: {r['url']}"
        for v in r.get("changedVariants") or []:                      # one size or colour came back
            if v.get("previousInStock") is False and v.get("inStock") is True:
                yield f"{r['name']} ({v['name']}) is back in stock at {v['price']}: {r['url']}"


rows = ApifyClient(os.environ["APIFY_TOKEN"]).run(sys.argv[1]).dataset().list_items(clean=True).items
alerts = list(restock_alerts(rows))
print(f"{len(rows)} changed rows, {len(alerts)} restock alert(s)")
for a in alerts:
    print(a)

On the monitor run heds8zid9ccytKKhQ (2026-10-20) it printed 2 changed rows, 0 restock alert(s): both changes in that run were price moves, which is the correct answer. To get a message out of it, use the same pattern as the webhook handler in the price drop alerts guide, which already forwards back-in-stock rows to Slack.

Option 2: diff two runs yourself

If you already scrape without monitor mode, compare two runs' datasets. This keys every product and every variant by product URL plus SKU:

python
"""Back-in-stock alerts: product level and size/colour level. pip install apify-client"""
import os
import sys

from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])


def products(run_id):
    items = client.run(run_id).dataset().list_items(clean=True).items
    return [r for r in items if r.get("recordType") == "product"]


def stock_map(rows):
    """(product URL, variant SKU or '') -> (inStock, label, price). inStock None = unknown, never 'out'."""
    out = {}
    for r in rows:
        out[(r["url"], "")] = (r.get("inStock"), r["name"], r.get("price"))
        for v in r.get("variants") or []:
            if v.get("sku"):
                label = " / ".join(f"{k} {x}" for k, x in (v.get("options") or {}).items()) or v.get("name")
                out[(r["url"], v["sku"])] = (v.get("inStock"), f"{r['name']} ({label})", v.get("price"))
    return out


def restocks(old_rows, new_rows):
    before, after = stock_map(old_rows), stock_map(new_rows)
    for key, (now, label, price) in after.items():
        was = before.get(key, (None,))[0]
        if was is False and now is True:              # only a known "out" -> known "in" counts
            yield label, price, key[0]


if __name__ == "__main__":
    old_run, new_run = sys.argv[1], sys.argv[2]
    for label, price, url in restocks(products(old_run), products(new_run)):
        print(f"BACK IN STOCK: {label} at {price} -> {url}")

Run against two of our test runs on 2026-10-17, one at 13:58 UTC (pm3sEpCiKnUa6DuAO) and one at 14:12 UTC (LP3JVIcrH0yubpdPR), it found a real size restock:

text
BACK IN STOCK: MASTERS COURT SUEDE PANELED SNEAKER - Sneaker low - grey fog (Size 43) at 96 -> https://www.zalando.de/polo-ralph-lauren-masters-lace-unisex-sneaker-low-grey-fog-po215o037-c11.html

Later runs that day show how brief it was: size 43 was still in stock at 14:12 UTC and out of stock again by 19:15 UTC. Restocks can be short, which is the whole case for automated alerts.

Avoid false alerts

Three things produced misleading flips in our test data:

  1. Unknown read as out. Handled above: never treat null as false.
  2. Different pages disagree about the same variant. On Walmart, a cable's 5-pack variant was OutOfStock on its own page but listed as InStock in the variant picker of its 1-pack and 2-pack pages, in the same run, consistently on 2026-10-17. Trust the variant's own page for variants you care about, key your state by the exact URL you watch (as both scripts do), and don't merge readings from different pages.
  3. Very short restocks. Re-check once before acting on an alert, or require two consecutive "in" readings if a false alarm is costly for you (for example, if the alert triggers an automatic purchase).

How often to check, and what it costs

Each run pays a start fee, so cadence drives cost as much as the number of products. With onlyChanges, an unchanged product is billed as "product checked" and a changed one as "product scraped". From the README's pricing table (pay per event):

FREE GOLD
Product checked (unchanged) $0.0012 $0.0002
Product scraped (changed or new) $0.0048 $0.0008
Actor start, per run $0.005 $0.005

For 50 product URLs on datacenter-reached sites, with nothing changing:

  • Hourly (24 runs): 1,200 checks. FREE: 1,200 × $0.0012 + 24 × $0.005 = $1.56/day. GOLD: 1,200 × $0.0002 + 24 × $0.005 = $0.36/day.
  • Every 30 minutes (48 runs): FREE $3.12/day, GOLD $0.72/day. At GOLD, a third of that is start fees.

So group products into one watchlist per cadence rather than one task per product, and keep the fast cadence for the few items that sell out. Sites reached through residential proxies or a browser (Noon, IKEA, Bloomingdale's and others in the README's table) also bill traffic per MB or a browser-page fee on each check, because the page is fetched even when nothing changed. Apify's free plan gives $5 of usage a month with no credit card; the actor has no in-actor free trial.

Cron for the two cadences in an Apify schedule: 0 * * * * (hourly) and */30 * * * * (48 runs a day).

During Black Friday and the holiday season

Doorbusters and limited bundles sell out and come back in waves during Black Friday week, and gift sizes and colours go out of stock in December. For that window, add a temporary second schedule to the same task, for example */15 * 26-30 11 *: every 15 minutes from Thanksgiving (Thursday 26 November 2026) to Cyber Monday (30 November), 480 runs in America/New_York. Use it only for a short list: 20 unchanged products cost 20 × $0.0002 + $0.005 = $0.009 per run on GOLD, or $4.32 for the 480 runs ($13.92 on FREE). Delete it afterwards. The holiday sales calendar lists each event's window, and the Christmas price tracker covers the December gift season.

Limits

  • Unsupported sites (eBay, Home Depot, Lowe's, Costco, Etsy, Best Buy and the others in the README matrix) can't be checked.
  • Stock is what the page states. Many stores show "in stock" without a count, so stockQty is often null.
  • Some sites show stock for a selected store or postcode only; the actor reads the page as it's served to its connection, so check that the market (proxyCountry) matches the shoppers you care about.

Related: scraping product variants, competitor price monitoring, and the e-commerce price tracker query builder.

Disclosure: I built the actor and this site. The diff logic (known out to known in, per variant) works with any scraper's output.

Sources: E-commerce Product Scraper Pro README (output fields, monitor mode, pricing, supported sites), Apify docs: Schedules, Apify pricing, and actor runs on 17-20 October 2026.

Quick answers

How do I get notified when an item is back in stock?

Use the store's own "notify me" button if it has one. For items it doesn't cover, or many items across stores, schedule a scraper to re-check the product pages, compare each product's and each variant's stock flag with the previous check, and send a message when one goes from out of stock to in stock.

How often should a back-in-stock checker run?

Hourly is a sensible default. Every 15-30 minutes is only worth it for scarce items that sell out quickly, and only during the window when restocks happen, because every run has a start cost on top of the per-product checks.

Can I track a specific size or colour, not just the product?

Yes, if the scraper returns variants. Each variant has its own SKU and stock flag, so you can alert on 'size 43 is back' even when the product as a whole never went out of stock.

Why does a scraper say the stock is unknown?

Some pages don't state stock in a readable way, or show a message such as 'cannot be delivered to your location' instead. Treat unknown as unknown, not as out of stock, or you'll send false back-in-stock alerts when the page starts showing stock again.

Open the free E-commerce price tracker tool →

Written by Omar Eldeeb, who also builds the tools and Apify actors mentioned here. Links to apify.com carry an affiliate parameter — see the disclosure.