Home / Blog / Amazon Price Drop Discord Alerts via API

Amazon Price Drop Discord Alerts via API

Get an Amazon price drop alert in Discord: poll the live offer listing for each ASIN, track a per-ASIN baseline price, confirm a lower reading on a second check, and post one webhook message with an embed per confirmed drop. Tells a real drop apart from a one-off reading, never alerts on a rise, and costs one credit per ASIN per check.

November 10, 2026
6 min read
SellerMagnet Team
Share & Bookmark
Terminal window showing an Amazon price drop alert a Python script posted to Discord

To get an Amazon price drop alert in Discord, poll /amazon-product-offers for each of your ASINs, compare the Buy Box total price with a stored baseline, and post to a Discord incoming webhook only after the same lower price shows up on two checks in a row. The script below sends one message per run with one embed per confirmed drop, never alerts on a price rise, and costs one credit per ASIN per check.

Key Takeaways

  • buyBox.totalPrice is the Buy Box total price; an empty buyBox means suppressed or no offers, not a price of zero.
  • A lower reading has to repeat on two checks in a row before it reaches Discord, the same convention as a Buy Box lost alert.
  • A price at or above the stored baseline updates the baseline silently; the script never alerts on a rise.
  • Discord's webhook endpoint answers 204 No Content on success, allows up to 10 embeds per message, and returns 429 with a retry_after value when rate-limited.
  • Each check costs 1 credit per ASIN; 502/503 retries are billed per attempt, a 500 is refunded, and a free 401/403 stops the run with one STOP message.

What does the offers endpoint tell you about the current price?

/amazon-product-offers reads one page of the live offer listing. buyBox is the featured offer with totalPrice as a decimal number including shipping. When buyBox is an empty object, there is no price to compare: either buyBoxSuppressed is true (offers exist but Amazon features none) or the offer list is empty. The script treats both cases the same way: it clears any pending drop for that ASIN without alerting, because a missing reading is not evidence of a price. Field details are in the Offer Listing API guide.

The five readings and what each one does to the pending drop.
ResponseReadingDiscord message
First sighting of an ASINno alertbaseline stored
Price below baseline, seen oncependingnone yet, waiting for a 2nd check
Same lower price seen twice in a rowconfirmed drop1 embed in the batched message
Price at or above the baselineno alertbaseline follows the price up
Buy Box suppressed or no offerspending clearednone

How do I build the Amazon price drop Discord alert?

read_price() turns one offers response into a total price or None, check() keeps a baseline and a pending-drop streak per ASIN in price_state.json, and post_to_discord() sends every confirmed drop as one message with up to 10 embeds. get_with_retry comes from retry.py and ApiError and new_session from sellermagnet.py, both from the Python quickstart. Malformed ASINs are skipped before any call, because the offers endpoint bills a typo like any other request. The first sighting of an ASIN only stores the baseline; a real drop measures against that baseline, or against the current price once it has moved, not a stale number from weeks earlier.

discord_price_alert.py - offers check, 2-check confirmation, one Discord message per run

import json
import os
import re

import requests

from retry import get_with_retry
from sellermagnet import ApiError, new_session

MARKETPLACE_ID = "ATVPDKIKX0DER"                  # amazon.com
ASINS = ["B0CL61F39H", "B0EXAMPLE2", "B0EXAMPLE3"]
DROP_CONFIRM = 2                                   # a lower price has to repeat on 2 checks before Discord hears it
STATE_FILE = "price_state.json"
ASIN_RE = re.compile(r"^[A-Z0-9]{10}$")            # offers calls are billed even for a typo, so filter first
WEBHOOK_URL = os.environ["DISCORD_WEBHOOK_URL"]
MAX_EMBEDS = 10                                    # Discord: at most 10 embeds per webhook message


def read_price(session, asin: str) -> tuple[float | None, str]:
    """(current buyBox total price or None, currency code). None covers a suppressed or empty buyBox alike."""
    data = get_with_retry(session, "amazon-product-offers", asin=asin, marketplaceId=MARKETPLACE_ID)
    box = data.get("buyBox") or {}
    price = box.get("totalPrice")
    currency = (data.get("currency") or {}).get("code", "")
    # buyBox.totalPrice is NOT rounded by the API (e.g. 444.97999999999996); round before comparing,
    # or float jitter on an unchanged price can misfire the confirmation check below.
    return (round(price, 2) if isinstance(price, (int, float)) else None), currency


def load_state() -> dict:
    try:
        with open(STATE_FILE) as f:
            return json.load(f)
    except FileNotFoundError:
        return {}


def save_state(state: dict) -> None:
    with open(STATE_FILE, "w") as f:
        json.dump(state, f)


def check(session, state: dict) -> list[dict]:
    """Returns one Discord embed per confirmed drop; updates state in place."""
    embeds = []
    for asin in ASINS:
        if not ASIN_RE.match(asin):
            continue                                      # never pay for a malformed ASIN
        try:
            price, currency = read_price(session, asin)
        except ApiError as err:
            if err.status in (401, 403):
                raise                                      # out of credits: stop the whole run, see main()
            continue                                       # a billed failure after retries: skip this ASIN only
        row = state.setdefault(asin, {"baseline": None, "pending": None, "streak": 0})
        if price is None:
            row["pending"], row["streak"] = None, 0        # suppressed/no offers: clears a pending drop, no alert
            continue
        if row["baseline"] is None:
            row["baseline"] = price                        # first sighting: store, never alert on it
            continue
        if price >= row["baseline"]:
            if price > row["baseline"]:
                row["baseline"] = price                     # price went back up: track it, never alert on a rise
            row["pending"], row["streak"] = None, 0
            continue
        # price < baseline
        if row["pending"] == price:
            row["streak"] += 1
        else:
            row["pending"], row["streak"] = price, 1
        if row["streak"] >= DROP_CONFIRM:
            pct = (row["baseline"] - price) / row["baseline"] * 100
            embeds.append({
                "title": asin,
                "url": f"https://www.amazon.com/dp/{asin}",
                "fields": [
                    {"name": "Was", "value": f"{row['baseline']:.2f} {currency}", "inline": True},
                    {"name": "Now", "value": f"{price:.2f} {currency}", "inline": True},
                    {"name": "Drop", "value": f"{pct:.1f}%", "inline": True},
                ],
            })
            row["baseline"], row["pending"], row["streak"] = price, None, 0
    return embeds


def post_to_discord(embeds: list[dict]) -> bool:
    """One message per run; Discord allows up to 10 embeds per message, so chunk if a run ever finds more."""
    ok = True
    for i in range(0, len(embeds), MAX_EMBEDS):
        chunk = embeds[i:i + MAX_EMBEDS]
        resp = requests.post(WEBHOOK_URL, json={"content": "Amazon price drop(s):", "embeds": chunk}, timeout=10)
        ok = ok and resp.status_code == 204
    return ok


def main():
    session = new_session()
    state = load_state()
    try:
        embeds = check(session, state)
    except ApiError:
        requests.post(WEBHOOK_URL, json={"content": "STOP: SellerMagnet API key rejected (401/403)."}, timeout=10)
        save_state(state)
        print(f"{len(ASINS)} checked, run stopped on 401/403")
        return
    sent = post_to_discord(embeds) if embeds else True
    save_state(state)
    print(f"{len(ASINS)} checked, {len(embeds)} drop(s) found, posted={sent}")


if __name__ == "__main__":
    main()

Illustrative run output ([discord] marks a webhook message, trimmed to its content line)

--- run 1 (illustrative ASINs and prices) ---
3 checked, 0 drop(s) found, posted=True
--- run 2, price drops ---
3 checked, 0 drop(s) found, posted=True
--- run 3, same lower price confirmed ---
3 checked, 3 drop(s) found, posted=True
[discord] DROP  B0CL61F39H: 19.99 USD -> 17.49 USD, 12.5%
[discord] DROP  B0EXAMPLE2: 19.99 USD -> 17.49 USD, 12.5%
[discord] DROP  B0EXAMPLE3: 19.99 USD -> 17.49 USD, 12.5%
--- run 4, price recovers above the old baseline ---
3 checked, 0 drop(s) found, posted=True

Trimmed offers response for a Buy Box drop (example values)

{
  "success": true,
  "data": {
    "asin": "B0CL61F39H",
    "buyBox": {"sellerId": "A2OTHERSELLER0", "totalPrice": 17.49},
    "buyBoxSuppressed": false,
    "currency": {"code": "USD", "symbol": "$", "name": "US Dollar"},
    "offers": [{"sellerId": "A2OTHERSELLER0", "condition": "New", "totalPrice": 17.49}]
  }
}
Blueprint flow: offers check per ASIN, two-check confirmation, then one Discord webhook message
A lower price has to repeat before it reaches Discord; a rise just updates the baseline.

How do I set up the Discord incoming webhook?

  1. Open channel settingsIn the Discord channel that should get the alerts, open Edit Channel > Integrations > Webhooks.
  2. Create a webhookClick New Webhook, give it a name, and save.
  3. Copy the URLCopy Webhook URL. It looks like https://discord.com/api/webhooks/<id>/<token>.
  4. Store the URL as a secretPut the URL into DISCORD_WEBHOOK_URL in an environment file that never goes into version control: the URL itself is the only credential needed to post.

Environment file and a cron entry for a 15-minute check (flock -n prevents overlapping runs)

# .env (keep it out of git: the webhook URL is the only credential needed to post)
export SELLERMAGNET_API_KEY="your-key-here"
export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/123456789012345678/XXXXXXXXXXXXXXXXXXXXXXXXXXXX"

# crontab -e: every 15 minutes, 3 ASINs = 288 credits a day; flock -n skips a run while the last one still runs
*/15 * * * * cd /opt/pricealert && . ./.env && flock -n .lock .venv/bin/python discord_price_alert.py >> alert.log 2>&1

Discord's own documentation (docs.discord.com/developers/resources/webhook, checked 2026-11-10) describes the contract the script relies on: executing a webhook with a plain POST answers 204 No Content on success, not 200 - only the ?wait=true variant returns 200 with the created message. A message needs at least one of content, embeds, components, file or poll; content is capped at 2000 characters, and a message can carry up to 10 embeds, each limited to 256 characters in its title, 1024 per field value and 6000 characters total. Discord's general rate-limit documentation (docs.discord.com/developers/topics/rate-limits) says an exceeded limit answers 429 with a retry_after value in the body, and its safety page (discord.com/safety/using-webhooks-and-embeds) separately states incoming webhooks are limited to about 30 messages per minute - comfortably above what a 15-minute poll of a handful of ASINs would ever send. The script treats any webhook response other than 204 as not delivered and leaves the baseline exactly where it was, so a drop that failed to post is included again on the next run while the lower price still holds.

How many credits does a price drop alert cost?

One offers call per ASIN per check, so the cost is the number of ASINs times the number of checks. With a 2-check confirmation the worst-case delay before Discord hears about a drop is about two intervals. Errors add to the bill in different ways: a 404 is billed once and not retried, a 502 or 503 is billed on every attempt, a 500 is refunded, and the free gate errors (400 for a missing parameter, 401, 403, 429) cost nothing. The Buy Box monitoring interval guide compares intervals in more detail, and Pricing lists the plans.

Credits per tracked ASIN by check interval (1 credit per offers call).
Check intervalCredits per dayCredits per 30 daysWorst-case alert delay
Every 15 minutes962,880about 30 minutes
Every hour24720about 2 hours
Every 4 hours6180about 8 hours
Once a day130about 2 days
Blueprint table of credits per tracked ASIN for four check intervals and the alert delay
Shorter intervals cost more credits and cut the delay; the confirmation doubles it.

Frequently Asked Questions

Why does a drop need to show up on 2 checks before Discord hears about it?

A single offers reading can misread a stock-out or a parsing gap as a lower price. Requiring the same lower price on two checks in a row filters a one-off reading at the cost of one interval.

Why does a price rise never trigger an alert?

The script only watches for drops. A rise silently becomes the new baseline, so the next real drop is measured from the current price, not a stale low from weeks ago.

What happens if more than 10 ASINs drop in the same run?

Discord allows at most 10 embeds per message, so the script sends one message per 10 drops instead of failing the whole batch.

Why does a 204 response mean the alert was posted?

Discord's webhook endpoint answers 204 No Content on a plain POST; only the ?wait=true variant returns 200 with the message body. Checking for 200 would treat every real success as a failure.

What happens when my credits run out?

The API answers 401 (or 403 during a burst), which is not billed. The script stops instead of checking the remaining ASINs and posts a STOP message on every run while the key stays rejected; it does not deduplicate, so budget one STOP message per interval until credits are added back.

Why filter the ASIN format before calling the API?

The offers endpoint bills a malformed ASIN like any other request, so a 10-character uppercase-alphanumeric check before the call avoids paying for a typo.

Bottom line: compare buyBox.totalPrice with a stored baseline, keep a per-ASIN streak, and post to Discord only when two checks agree, in one message per run with up to 10 embeds. To watch who holds the Buy Box instead of what it costs, use the Buy Box lost Slack alert; a free account includes 150 credits to try the alert.

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