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.
| Response | Reading | Discord message |
|---|---|---|
| First sighting of an ASIN | no alert | baseline stored |
| Price below baseline, seen once | pending | none yet, waiting for a 2nd check |
| Same lower price seen twice in a row | confirmed drop | 1 embed in the batched message |
| Price at or above the baseline | no alert | baseline follows the price up |
| Buy Box suppressed or no offers | pending cleared | none |
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}]
}
}

How do I set up the Discord incoming webhook?
- Open channel settingsIn the Discord channel that should get the alerts, open Edit Channel > Integrations > Webhooks.
- Create a webhookClick New Webhook, give it a name, and save.
- Copy the URLCopy Webhook URL. It looks like https://discord.com/api/webhooks/<id>/<token>.
- 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.
| Check interval | Credits per day | Credits per 30 days | Worst-case alert delay |
|---|---|---|---|
| Every 15 minutes | 96 | 2,880 | about 30 minutes |
| Every hour | 24 | 720 | about 2 hours |
| Every 4 hours | 6 | 180 | about 8 hours |
| Once a day | 1 | 30 | about 2 days |

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.