Home / Blog / Amazon Competitor Out-of-Stock Alerts via API: Know When a Listing Runs Dry

Amazon Competitor Out-of-Stock Alerts via API: Know When a Listing Runs Dry

Get an alert when a competitor's Amazon listing runs out of offers, loses its featured offer or Amazon itself stops selling it: one offers call per check, a state per listing, and a second check before alerting so a single empty page never triggers a false alarm.

September 26, 2026
4 min read
SellerMagnet Team
Share & Bookmark
Blueprint flow from watched ASINs through offer checks and a confirmation step to out-of-stock alerts

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.

Listing states derived from one offers call (September 2026).
StateSeen asMeans
In stockA featured offer, New offers presentNormal
No featured offerOffers exist, buyBoxSuppressed trueNobody wins the Buy Box
Used onlyOffers exist, none NewNew stock sold out
No offersBuyBox {}, offers []Nobody sells it right now
Amazon leftNo New offer from AmazonFirst-party stock gone
Listing gone404 Invalid productPage removed
Blueprint table of listing states from the offers endpoint, how each is detected and what it means
Six states, one call per check.

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.

Blueprint bar chart of credits per watched ASIN per month for four check intervals
Credits per watched ASIN per month; the confirmation doubles the alert delay, not the cost.

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.

Ready to Extract Amazon Data at Scale?

Start building with SellerMagnet API today. Real-time product data, competitive pricing, and review analytics at your fingertips.

150 free API credits • No credit card required • Cancel anytime