To get an alert when a competitor runs out of stock on Amazon, check the listing's offers with /api/amazon-product-offers on a schedule and classify each answer: in stock, no featured offer, used only, or no offers at all - and whether Amazon itself still sells it. Alert when the state changes and the next check confirms it. The confirmation matters: a single empty page can be a scraping miss, and a false "out of stock" alert is worse than a late one.
Key Takeaways
- A listing with no offers at all comes back as buyBox {} and offers [] - the clearest out-of-stock signal.
- buyBoxSuppressed true means offers exist but nobody holds the Buy Box - often a pricing or eligibility issue, not a stock-out.
- Amazon leaving shows as no New offer from "Amazon" - conclusive when Amazon held the Buy Box, since offers[] is the first page only.
- Alert only when the same new state appears on two checks in a row; one empty page is not a stock-out.
- Every check costs one credit per ASIN: checking every six hours is 120 credits per ASIN a month.
How can I tell that a competitor is out of stock?
The offers endpoint reads the listing's offer panel, so a stock-out shows up as missing offers rather than a stock number - the inventory field is a cart limit, not stock, as the offer listing guide explains. Six states cover what matters to a competitor. offers[] is the first page of the offer listing, so "Amazon left" and "used only" are conclusive only when the page is short; a pricier Amazon or New offer can sit below it.
| State | Seen as | Means |
|---|---|---|
| In stock | A featured offer, New offers present | Normal |
| No featured offer | Offers exist, buyBoxSuppressed true | Nobody wins the Buy Box |
| Used only | Offers exist, none New | New stock sold out |
| No offers | BuyBox {}, offers [] | Nobody sells it right now |
| Amazon left | No New offer from Amazon | First-party stock gone |
| Listing gone | 404 Invalid product | Page removed |

How do I build out-of-stock alerts in Python?
status() turns one offers call into a small dict, check() compares it with the confirmed state of each listing. A different state is first stored as pending; only if the next check shows it again is it confirmed and reported. A 404 becomes the state "listing gone"; any other failed call is skipped and changes nothing. The state file is written to a temporary file and swapped in, so a crash never leaves it half-written. If most of your watch list flips to "no offers" in the same run, suspect a scraping problem rather than a wave of stock-outs. get(), new_session() and ApiError come from the Python quickstart.
stock_watch.py - classify, confirm, alert
"""Alert when a watched listing runs out of offers, loses its featured offer, or Amazon leaves it."""
import json
import sys
from pathlib import Path
import requests
from sellermagnet import ApiError, get, new_session
STATE = Path("stock_state.json")
def status(asin: str, marketplace_id: str) -> dict | None:
try:
d = get(new_session(), "amazon-product-offers", asin=asin, marketplaceId=marketplace_id)
except (ApiError, requests.RequestException) as err:
if getattr(err, "status", None) == 404: # delisted: a state of its own, and still billed
return {"state": "listing gone", "amazon": False, "new_sellers": 0}
print(f"{asin}: skipped ({getattr(err, 'status', type(err).__name__)})", file=sys.stderr)
return None
offers = d.get("offers") or []
new = [o for o in offers if o.get("condition") == "New"]
if not offers:
state = "no offers"
elif d.get("buyBoxSuppressed") or not d.get("buyBox"):
state = "no featured offer"
elif not new:
state = "used only"
else:
state = "in stock"
return {"state": state, "amazon": any(o.get("sellerName") == "Amazon" for o in new),
"new_sellers": len({o.get("sellerId") for o in new})}
def describe(asin: str, old: dict, new: dict) -> list[str]:
lines = []
if old["state"] != new["state"]:
lines.append(f"{asin}: {old['state']} -> {new['state']} ({new['new_sellers']} new-condition sellers)")
if old["amazon"] != new["amazon"]:
lines.append(f"{asin}: Amazon {'joined' if new['amazon'] else 'left'} the listing")
return lines
def check(listings: list[tuple[str, str]]) -> list[str]:
state = json.loads(STATE.read_text()) if STATE.exists() else {}
alerts = []
for asin, marketplace_id in listings:
now = status(asin, marketplace_id)
if now is None:
continue
key, entry = f"{marketplace_id}:{asin}", state.get(f"{marketplace_id}:{asin}", {})
confirmed, pending = entry.get("confirmed"), entry.get("pending")
signature = lambda s: (s["state"], s["amazon"]) if s else None
if confirmed is None or signature(now) == signature(confirmed):
state[key] = {"confirmed": now} # baseline, or nothing changed
elif signature(now) != signature(pending):
state[key] = {"confirmed": confirmed, "pending": now} # first sighting: wait for the next check
else:
alerts += describe(asin, confirmed, now) # seen twice in a row: report it
state[key] = {"confirmed": now}
tmp = STATE.with_suffix(".tmp") # write, then swap: a crash never leaves half a file
tmp.write_text(json.dumps(state, indent=1))
tmp.replace(STATE)
return alerts
if __name__ == "__main__":
for line in check([("B0CL61F39H", "ATVPDKIKX0DER"), ("B0CLTBHXWQ", "APJ6JRA9NG5V4")]):
print(line)
Example alerts after two confirming checks
B0CL61F39H: in stock -> no offers (0 new-condition sellers)
B0CL61F39H: Amazon left the listing
How often should I check, and what does it cost?
Each check is one credit per ASIN, and a state change needs two checks to confirm, so an alert arrives one to two intervals after the change. Every six hours means an alert within twelve hours at 120 credits per ASIN a month; daily checks cost 30 but can take up to two days to confirm a stock-out. Watch the few competitors that set your price, not the whole category. For Buy Box owner changes rather than stock, the polling guide weighs intervals in detail.

What should I do when a competitor runs out?
A confirmed stock-out is a short window: shoppers who wanted that product see fewer choices. Typical reactions are reviewing your price, raising ad bids on the shared keywords and making sure your own stock lasts. Check the listing change monitor too - a listing that comes back with new content may be a relaunch rather than a restock - and watch each child ASIN of a variation family separately, since stock-outs happen per child.
One listing's state from the command line
curl -sG "https://sellermagnet-api.com/api/amazon-product-offers" \
-H "X-Api-Key: $SELLERMAGNET_API_KEY" --max-time 120 \
--data-urlencode "asin=B0CL61F39H" \
--data-urlencode "marketplaceId=ATVPDKIKX0DER" \
| jq -r 'if .success then (.data | (.offers // []) as $o
| ($o | map(select(.condition == "New"))) as $new
| if ($o | length) == 0 then "no offers"
elif .buyBoxSuppressed or ((.buyBox // {}) | length) == 0 then "no featured offer"
elif ($new | length) == 0 then "used only"
else "in stock: \($new | length) New offers, Amazon among them: \(any($new[]; .sellerName == "Amazon"))" end)
else error(.message) end'
Frequently Asked Questions
Can I get alerted when a competitor on Amazon runs out of stock?
Yes. Check the listing's offers on a schedule and alert when it changes to no offers or loses its New offers, confirmed on the next check.
Does the API return a competitor's stock level?
Not from the offers endpoint: its inventory field is a cart limit. The statistics endpoint returns a recorded stockHistory per New-condition seller on 11 marketplaces; treat it as indicative.
Why wait for a second check?
A single empty page can be a scraping miss. Requiring the same state twice in a row prevents false out-of-stock alerts.
Is a suppressed Buy Box a stock-out?
Not necessarily. buyBoxSuppressed means offers exist but none is featured, often because of price or eligibility, not missing stock.
How many credits does monitoring cost?
One per ASIN and check. Every six hours is 120 credits per ASIN a month; daily is 30.
Bottom line: one offers call per check, five states, and a confirmation on the next check turn a watch list into reliable stock-out alerts. The product offers page lists every field, and a free account includes 150 credits.