
Quick answer: Tracking stock across retailers means polling each product page on a sensible cadence, reading availability from structured signals rather than page text where possible, and requiring confirmation before you alert. The failure mode isn't missing a restock; it's false positives from cached pages, geo-dependent stock, and "add to cart" buttons that lie. Design for skepticism and the rest is a cron job.
I've built stock trackers for my own purchases and for clients, and the lesson is always the same: detecting "in stock" is easy, detecting it correctly is the work.
The Signals, Ranked
Product pages announce availability in several places, and they are not equally trustworthy:
| Signal | Where it lives | Reliability | Notes |
|---|---|---|---|
schema.org availability |
JSON-LD in page source | High | InStock, OutOfStock, PreOrder, BackOrder |
| Store inventory API | XHR the page calls | High | Great when stable, unofficial and can change |
| "Add to cart" button state | Rendered DOM | Medium | Buttons exist while disabled; check state, not presence |
| Stock text ("Only 3 left") | Rendered DOM | Medium | Wording changes with A/B tests |
| Delivery date widget | Rendered DOM | Low | Often shown even when unavailable |
| Third-party stock sites | Their pages | Low | You inherit their lag and their bugs |
Rule of thumb: parse JSON-LD first, fall back to rendered-DOM extraction, and treat free-text as a tiebreaker rather than a source of truth.
Fetching the Page (Rendered, Not Raw)
Availability is exactly the kind of data that gets injected client-side, so you generally need the rendered page. I use a fetch API rather than maintaining browsers; one call returns the extracted fields:
import requests
def check_stock(url: str) -> dict:
r = requests.post(
"https://link.sc/v1/fetch",
headers={"Authorization": "Bearer lsc_your_key"},
json={
"url": url,
"format": "json",
"schema": {
"title": "product title",
"availability": "one of: in_stock, out_of_stock, preorder, unknown",
"price": "current price as a number, null if not shown",
"cart_enabled": "true if the add-to-cart button is clickable",
"stock_note": "any stock quantity text on the page, verbatim",
},
},
timeout=60,
)
r.raise_for_status()
return r.json()
Asking for both availability and cart_enabled is deliberate. When two independent signals agree, your confidence is real; when they disagree, you want to know that instead of alerting on the optimistic one. The link.sc docs cover the schema extraction options in detail.
Polling Cadence: Slower Than You Think
Your instinct will be to poll every minute. Resist it, for three reasons: cost, politeness, and the fact that it rarely changes outcomes.
- High-demand drops (consoles, GPUs, sneakers) sell out in minutes, and for those, even 60-second polling loses to purpose-built communities. Be realistic about what polling can win.
- Normal restocks happen when a warehouse batch lands, typically once a day or less. Checking every 15 to 60 minutes catches these with hours to spare.
- Long-tail items change state weekly. Daily checks are plenty.
A tiered schedule is the honest design: a small hot list at 5 to 15 minutes, a warm list hourly, everything else daily. And add jitter so you're not the client that hits every product page at exactly :00. From the retailer's perspective, your tracker should look like a mildly curious customer, not a monitoring system. That's part of being a polite scraper generally, which I've written more about in ethical web scraping compliance best practices.
Avoiding False Positives
Every stock tracker eventually sends the embarrassing "IT'S IN STOCK" alert for an item that is not, in fact, in stock. The usual causes:
- CDN and cache lag. You fetched a cached page from before the sellout. Mitigation: treat a transition as provisional until a second fetch confirms it.
- Geo-dependent stock. In stock in one region's warehouse, not another's. Mitigation: fetch from a consistent location and label alerts with it.
- Variant confusion. Size 9 restocked, size 11 didn't, and you tracked the page instead of the variant. Track variant-specific URLs or extract per-variant state.
- Backorder and preorder states. "Available to order" is not "in stock." Keep them as distinct states in your enum.
- Marketplace sellers. "In stock from a third-party seller at twice the price" is technically true and practically useless. Extract the price too, and alert only within a price ceiling.
The confirmation pattern costs you a few minutes of latency and eliminates most of the noise:
def confirmed_transition(sku: str, url: str) -> bool:
first = check_stock(url)
if first["availability"] != "in_stock":
return False
if db.last_state(sku) == "in_stock":
return False # not a transition, no alert
time.sleep(180)
second = check_stock(url)
return (second["availability"] == "in_stock"
and second["cart_enabled"])
Alerting: State Machines, Not Notifications
Store the state per (SKU, retailer) and alert only on transitions. Out-of-stock to in-stock is the alert people want; in-stock to out-of-stock is useful for your own analytics; unknown states should page you, not your users, because they usually mean the page layout changed or the fetch got blocked.
Route alerts wherever you already live: a Slack webhook, email, or SMS for the truly urgent list. Include the price, the retailer, the fetch timestamp, and the URL in every alert so the human can verify in one click. The plumbing here is identical to any page-change monitor, and I covered the alerting patterns in monitor web page changes and get alerts.
Compliance Note
Stock status is public, factual data, which puts this on the safer end of scraping. Still: respect robots.txt, honor retailer terms where they prohibit automated access, keep request rates low enough to be a rounding error in their traffic, and never automate purchases against a site that forbids it. Tracking availability for your own purchasing decisions or market research is one thing; building a bot that snipes limited drops is a different activity with different consequences, and plenty of retailers ban accounts over it.
The Bottom Line
A trustworthy stock tracker is a small system: rendered fetches, structured availability signals, tiered polling with jitter, two-signal confirmation, and transition-based alerts. None of it is hard once you accept that the enemy is false positives, not missed restocks. Build for skepticism and your alerts will actually get trusted.
Want stock checks without running browsers? link.sc fetches rendered product pages and returns availability as clean JSON. Start free with 500 credits a month.