Home / Blog / Amazon Negative Feedback Alert: E-mail New 1- and 2-Star Ratings

Amazon Negative Feedback Alert: E-mail New 1- and 2-Star Ratings

Build an Amazon negative feedback alert: one daily call to the seller feedback endpoint, a saved baseline of counts and recent entries, and an e-mail for every new 1- or 2-star rating with its text. Handles the five-entry limit, the API's 2-star split and failed checks; tested Python included.

October 23, 2026
5 min read
SellerMagnet Team
Share & Bookmark
Terminal window running a Python script that reports new negative Amazon seller feedback

An Amazon negative feedback alert needs one API call per check: /amazon-seller-review returns your seller feedback counts and the five most recent feedback entries. Save both as a baseline, compare each later check with it, and e-mail every new 1- or 2-star entry with its text. The lifetime 1-star count catches ratings that already scrolled out of the five-entry list, and a failed check never touches the baseline.

Key Takeaways

  • /amazon-seller-review returns positive, neutral and negative feedback as counts for 30, 90 and 365 days and lifetime, plus up to five recent entries with text and star rating.
  • The API files 2 stars under neutral and only 1 star under negative, while Amazon's feedback page calls both negative, so the script reads the star rating of each entry.
  • The recent list holds five entries, so the lifetime 1-star count is the backstop for ratings that scrolled out between two checks.
  • The first run only saves a baseline: feedback that existed before you started is not news.
  • A daily check costs 1 credit, about 30 a month; a 404 for a wrong seller ID is billed, a 401 is free.

What does the API count as negative feedback?

positiveRating, neutralRating and negativeRating are counts, not percentages, each for 30d, 90d, 365d and lifetime. The API counts 4 and 5 stars as positive, 2 and 3 stars as neutral and 1 star as negative. Amazon's own feedback page calls 1 and 2 stars negative, so a 2-star rating shows up in neutralRating, and only the star rating of an entry in recentFeedback tells it apart from a 3-star one. Each entry has text, rating (1 to 5, or "N/A" when the star text could not be read), starRatingText and dateRated, the rater line with name and date. The seller feedback API guide covers every field.

Star ratings, the API field that counts them, Amazon's label and the alert.
RatingCounted inAmazon's feedback pageAlert
5 or 4 starspositiveRatingpositiveno
3 starsneutralRatingneutralno
2 starsneutralRatingnegativeyes, from the recent list
1 starnegativeRatingnegativeyes, list and lifetime count

How does the Amazon negative feedback alert find new ratings?

read_feedback() makes one call and keeps two things: the lifetime 1-star count and the recent entries. Entries have no ID, so entry_key() hashes the rater line and the text. find_alerts() reports every unseen entry rated 1 or 2, then compares the lifetime 1-star count with the baseline: if it rose by more than the new 1-star entries explain, the rest scrolled out of the list and the e-mail says how many. get_with_retry comes from retry.py and ApiError and new_session from sellermagnet.py, both from the Python quickstart. The baseline is only saved after the e-mail went out, so an alert that could not be sent comes back on the next run.

feedback_alert.py - one feedback call, baseline compare, one e-mail per run

import hashlib
import json
import os
import smtplib
import sys
from email.message import EmailMessage

import requests

from retry import get_with_retry
from sellermagnet import ApiError, new_session

MARKETPLACE_ID = "ATVPDKIKX0DER"                 # amazon.com; feedback is counted per store
SELLER_ID = os.environ["MY_SELLER_ID"]          # your own seller ID; no default on purpose
NEGATIVE_MAX = 2                                 # Amazon's feedback page calls 1 and 2 stars negative
STATE_FILE = "feedback_state.json"


def entry_key(entry: dict) -> str:
    """recentFeedback entries carry no ID: the rater line (name and date) plus the text identify one."""
    raw = f"{entry.get('dateRated')}|{entry.get('text')}"
    return hashlib.sha1(raw.encode("utf-8")).hexdigest()[:16]


def read_feedback(session) -> dict:
    data = get_with_retry(session, "amazon-seller-review", sellerId=SELLER_ID, marketplaceId=MARKETPLACE_ID)
    fb = data.get("feedback") or {}
    return {"one_star": int((fb.get("negativeRating") or {}).get("lifetime") or 0),   # 1-star count, not a %
            "recent": fb.get("recentFeedback") or []}                              # the last 5 entries at most


def find_alerts(state: dict, now: dict) -> list[str]:
    seen, lines, new_one_star = set(state["seen"]), [], 0
    for entry in now["recent"]:
        rating = entry.get("rating")                                 # 1-5, or "N/A" when unreadable
        if entry_key(entry) in seen or not isinstance(rating, int) or rating > NEGATIVE_MAX:
            continue
        new_one_star += rating == 1
        lines.append(f"{rating}-star: \"{entry.get('text', '')[:300]}\" {entry.get('dateRated', '')}")
    # the list shows 5 entries; the lifetime count catches 1-star ratings that already scrolled out of it
    extra = now["one_star"] - state["one_star"] - new_one_star
    if extra > 0:
        lines.append(f"{extra} more 1-star rating(s) than the recent list shows")
    return lines


def send_mail(lines: list[str]) -> bool:
    msg = EmailMessage()
    msg["Subject"] = f"Amazon feedback alert: {len(lines)} new negative item(s)"
    msg["From"], msg["To"] = os.environ["SMTP_USER"], os.environ["ALERT_TO"]
    msg.set_content("\n".join(lines) + f"\n\nSeller {SELLER_ID} on marketplace {MARKETPLACE_ID}\n")
    try:
        with smtplib.SMTP_SSL(os.environ["SMTP_HOST"], int(os.environ.get("SMTP_PORT", "465")), timeout=30) as smtp:
            smtp.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
            smtp.send_message(msg)
    except (smtplib.SMTPException, OSError) as err:
        print(f"mail not sent ({type(err).__name__}): the alert is sent again on the next run")
        return False
    return True


def load_state() -> dict:
    try:
        with open(STATE_FILE, encoding="utf-8") as fh:
            return json.load(fh)
    except FileNotFoundError:
        return {}


def save_state(state: dict) -> None:
    tmp = STATE_FILE + ".tmp"
    with open(tmp, "w", encoding="utf-8") as fh:
        json.dump(state, fh, indent=1)
    os.replace(tmp, STATE_FILE)                     # a crash mid-write keeps the last good baseline


def main() -> int:
    state = load_state()
    try:
        now = read_feedback(new_session())
    except ApiError as err:                          # 404 = wrong seller ID for this store (billed)
        print(f"check failed: {err}; baseline kept")
        return 1
    except requests.RequestException as err:
        print(f"check failed: {type(err).__name__} after retries; baseline kept")
        return 1
    keys = [entry_key(e) for e in now["recent"]]
    if not state:                                    # first run: today's feedback is the baseline, not news
        save_state({"one_star": now["one_star"], "seen": keys})
        print(f"baseline saved: {now['one_star']} 1-star rating(s) lifetime, {len(keys)} recent entries")
        return 0
    lines = find_alerts(state, now)
    if lines and not send_mail(lines):
        return 1                                     # baseline unchanged, so the same alert comes back
    one_star = now["one_star"]
    if one_star < state["one_star"]:                 # removed feedback or a partly read page: keep the bar
        print(f"1-star count fell from {state['one_star']} to {one_star}; baseline kept")
        one_star = state["one_star"]
    save_state({"one_star": one_star, "seen": list(dict.fromkeys(keys + state["seen"]))[:50]})
    print(f"{len(lines)} new negative item(s){', mailed' if lines else ''}")
    for line in lines:
        print("  " + line)
    return 0


if __name__ == "__main__":
    sys.exit(main())

First run and the next day (illustrative feedback; [mail] = the message the SMTP server got)

$ python feedback_alert.py      # first run (illustrative feedback)
baseline saved: 12 1-star rating(s) lifetime, 5 recent entries
$ python feedback_alert.py      # next day: 1 one-star and 1 two-star in the list, 2 new one-stars counted
[mail] Subject: Amazon feedback alert: 3 new negative item(s)
3 new negative item(s), mailed
  2-star: "Not as described, the charger was missing." By K. Example on October 22, 2026.
  1-star: "Arrived two weeks late and the box was crushed." By J. Example on October 22, 2026.
  1 more 1-star rating(s) than the recent list shows

Trimmed /amazon-seller-review response (example values)

{
  "success": true,
  "data": {
    "sellerId": "A1MYSELLER0001",
    "marketplaceId": "ATVPDKIKX0DER",
    "sellerFullName": "My Store",
    "feedback": {
      "negativeRating": {"30d": 1, "90d": 2, "365d": 5, "lifetime": 14},
      "neutralRating": {"30d": 1, "90d": 2, "365d": 4, "lifetime": 20},
      "recentFeedback": [
        {"text": "Not as described, the charger was missing.", "rating": 2,
         "starRatingText": "2 out of 5 stars", "dateRated": "By K. Example on October 22, 2026."}
      ]
    }
  }
}
Blueprint flow: daily feedback call, compare with the saved baseline, e-mail new negative ratings
Only new 1- and 2-star items reach the e-mail; failed calls leave the baseline alone.

How do I run the feedback check every day?

Put the key, your seller ID and the mail settings into an environment file and start the script from cron. MY_SELLER_ID has no default on purpose: the script refuses to start without it instead of checking the wrong seller. If you do not know the ID, the seller ID lookup guide shows where to find it. Feedback is counted per store, so one run checks one marketplace; copy the MARKETPLACE_ID line into a second script for a second store.

Environment file and a daily cron entry

# .env (keep it out of git: it holds the API key and the mail password)
export SELLERMAGNET_API_KEY="your-key-here"
export MY_SELLER_ID="A1MYSELLER0001"        # your seller ID on this store
export SMTP_HOST="smtp.example.com"         # your mail provider's SMTP server
export SMTP_PORT="465"                      # SMTP submission over implicit TLS
export SMTP_USER="alerts@example.com"
export SMTP_PASSWORD="app-password-here"
export ALERT_TO="you@example.com"

# crontab -e: once a day at 07:00 = 30 credits a month
0 7 * * * cd /opt/feedback && . ./.env && .venv/bin/python feedback_alert.py >> feedback.log 2>&1

smtplib.SMTP_SSL opens the connection with TLS from the first byte, and its default port is 465 (Python documentation, checked 2026-10-23); RFC 8314 registers port 465 for message submission over implicit TLS. Use the host, port and app password your mail provider documents. A login or connection error is reported without the password, the baseline stays unchanged, and the same alert goes out on the next run.

How many credits does a feedback alert cost?

One /amazon-seller-review call per check, whatever the number of entries. Billing follows the usual rules: a 404 is billed once and not retried, a 502 or 503 is billed on every attempt of get_with_retry, a 500 is refunded, and the free gate errors (400 for a missing parameter, 401, 403, 429) cost nothing. Plans are on Pricing.

Credits for one seller ID on one store, by check interval.
Check intervalCredits per dayCredits per 30 days
Once a day130
Twice a day260
Every 6 hours4120
Every hour24720
Blueprint table of star ratings, the API field that counts them and Amazon's feedback label
Two stars are neutral in the API and negative on Amazon's page; the alert covers both.

What can the alert not see?

Three limits come from the data. The recent list shows five entries, so a 2-star rating that scrolls out of it between two checks is missed, because its count sits in neutralRating next to the 3-star ones. A falling count (removed feedback, or a page that was only partly read) never lowers the baseline, so it cannot trigger a false alarm later; the script prints the drop instead. And an entry whose rating is "N/A" is only caught through the 1-star count. If you also want to know when an unauthorised seller shows up on your listings, the hijacker alert watches the offer list for that.

Frequently Asked Questions

Does Amazon count 2-star feedback as negative?

Amazon's feedback page does: it calls 1 and 2 stars negative and 3 stars neutral. The API's negativeRating counts only 1-star ratings and files 2 stars under neutralRating, so the script reads the star rating of each recent entry.

How often should I check my seller feedback?

Once a day costs about 30 credits a month. Check more often if more than five new feedback entries can arrive between two checks, because the recent list only shows the last five.

Why did the first run send no e-mail?

The first run saves the current counts and the recent entries as the baseline. Feedback that existed before you started is not new, so only later checks can send an alert.

What does a 404 from the feedback endpoint mean?

The seller ID has no feedback page on that store: a typo, the wrong marketplace, or Amazon's own seller ID. A 404 is billed, so the script reports it, keeps the baseline and does not retry.

Does the alert cover product reviews?

No. Seller feedback rates the seller, product reviews rate the item, and they live on different pages. This endpoint reads the seller feedback page only.

Bottom line: one feedback call a day, a baseline of the lifetime 1-star count and the recent entries, and an e-mail only for new 1- and 2-star ratings is a complete negative feedback alert for about 30 credits a month. A free account includes 150 credits to try it.

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