EU price reduction compliance under Article 6a of Directive 98/6/EC (added by the Omnibus Directive 2019/2161) means the "was" price shown with a reduction must be the LOWEST price YOU, the trader, applied during at least the 30 days before the reduction, never a cross-seller Buy Box price. Pull your own seller offer history from /amazon-product-statistics, keep only your own sellerId, and compare its 30-day low against the prior price you plan to advertise before you publish the reduction.
Key Takeaways
- Article 6a needs the lowest price the trader itself applied in the 30 days before a reduction, not a price any seller held the Buy Box at.
stats.buyBoxPriceHistoryon/amazon-product-statisticsmixes whichever seller held the Buy Box at each point, which can be a competitor: it is a buyer-side check, not a compliance check.- The compliance input is
data.offers[sellerId].priceHistory, filtered to your own seller ID, landed as item price plus shipping, over the trailing 30 days. - A seller ID without tracked offer history, or a tracking window shorter than 30 days, cannot be audited from the API alone: fall back to your own sales records.
- This is a self-audit tool, not legal advice: get legal advice for your specific markets before relying on it.
Why the Buy Box price is the wrong input for this check
Article 6a of the EU Price Indication Directive (98/6/EC), added by the Omnibus Directive 2019/2161 and explained in the Commission's 2021 guidance, says an announcement of a price reduction must state the prior price: the lowest price the trader applied during at least the 30 days before the reduction. That is a statement about ONE seller's own pricing history, not about who had the Buy Box. stats.buyBoxPriceHistory on /amazon-product-statistics is a landed price series (item plus shipping) of whichever seller held the Buy Box at each recorded point, and the Buy Box holder changes over time; using it as "the lowest price in 30 days" can quietly pick up a competitor's price instead of your own. The compliance input is your own seller offer history: data.offers[sellerId].priceHistory, keyed by seller ID, filtered to the ID you sell under on that marketplace. Field details are on the Product Statistics endpoint page.
| Field | Whose price is it | Use it for |
|---|---|---|
| stats.buyBoxPriceHistory | Whichever seller held the Buy Box at that time (can change) | A buyer-side "is this deal real" check across sellers |
| data.offers[sellerId].priceHistory | Exactly the seller ID you request | An Art 6a self-audit of YOUR own 30-day low |
| buyBoxSellerIdHistory | Who held the Buy Box at each point (sellerId or -1/-2) | Confirming which price points in buyBoxPriceHistory were actually yours |
How does the self-audit script work?
Call /amazon-product-statistics once per ASIN with your own seller ID, read data.offers[sellerId].priceHistory (entries are [timestamp, price, shipping] in integer minor units, UTC), keep only points from the last 30 days, land each one as price plus shipping and drop points where price is -2 (unreadable) or shipping is -1 (unspecified, so the landed price cannot be formed). The lowest of what remains is the price you are allowed to advertise as the prior price. get_with_retry comes from retry.py, ApiError and new_session from sellermagnet.py, both published in the Python quickstart. The same history also backs the Buy Box price history guide and the Buy Box holder history guide, which read the cross-seller series this post deliberately avoids for compliance.
Install requests, set the key, run the audit
pip install requests
export SELLERMAGNET_API_KEY="your-key-here"
python3 eu_price_reduction_audit.py
eu_price_reduction_audit.py: self-audit a planned EU price reduction against your own 30-day low
"""Self-audit a planned Amazon EU price-reduction announcement against Art 6a of Directive 98/6/EC (added by the
Omnibus Directive 2019/2161): the "was" price shown with a reduction must be the LOWEST price the trader ITSELF
applied in the 30 days before the reduction. This is a seller-side check on the seller's own price, not a
buyer-side Buy Box comparison: data.offers[sellerId].priceHistory from /amazon-product-statistics is filtered to
the seller's own sellerId, never buyBoxPriceHistory, which mixes whichever seller held the Buy Box at each point
in time and can be a competitor's price, not the trader's own.
"""
import re
import sys
from datetime import datetime, timedelta, timezone
import requests
from retry import get_with_retry
from sellermagnet import ApiError, new_session
WINDOW_DAYS = 30
ASIN_RE = re.compile(r"^[A-Z0-9]{10}$")
def audit_one(session, asin, marketplace_id, seller_id, planned_was_cents, now=None):
"""Returns one row: verdict is PASS, FAIL or an explained skip. Never guesses past what the data shows."""
now = now or datetime.now(timezone.utc)
row = {"asin": asin, "seller_id": seller_id, "planned_was": planned_was_cents}
if not ASIN_RE.match(asin):
row["verdict"] = "skipped: malformed ASIN, not sent (saves a billed 400)"
return row
try:
data = get_with_retry(session, "amazon-product-statistics", asin=asin, marketplaceId=marketplace_id)
except ApiError as err:
row["verdict"] = f"error: {err.status} {err}"
return row
except (requests.ConnectionError, requests.Timeout) as err:
row["verdict"] = f"error: connection failed after retries ({type(err).__name__}); check this row again"
return row
own = (data.get("offers") or {}).get(seller_id)
if not own or not own.get("priceHistory"):
row["verdict"] = "no seller history here; audit manually"
return row
cutoff = now - timedelta(days=WINDOW_DAYS)
landed = []
for ts, price, shipping in own["priceHistory"]:
when = datetime.strptime(ts, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
if when < cutoff:
continue
if price == -2 or shipping == -1: # unreadable price / unspecified shipping: landed price unknown
continue
landed.append(price + shipping)
tracking_since = data.get("trackingSince")
short_window = bool(tracking_since) and datetime.strptime(
tracking_since, "%Y-%m-%d").replace(tzinfo=timezone.utc) > cutoff
row["short_window"] = short_window
if not landed:
row["verdict"] = "no usable price in the last 30 days; audit manually"
return row
lowest = min(landed)
row["lowest_30d"] = lowest
if planned_was_cents > lowest:
row["verdict"] = f"FAIL: show {lowest} as the prior price, not {planned_was_cents}"
else:
row["verdict"] = "PASS: proposed prior price is at or below your real 30-day low"
return row
def audit_batch(session, items, now=None):
"""items: (asin, marketplace_id, seller_id, planned_was_cents). Dedupes on (asin, marketplace, seller)."""
seen = set()
rows = []
for asin, marketplace_id, seller_id, planned_was_cents in items:
key = (asin, marketplace_id, seller_id)
if key in seen:
rows.append({"asin": asin, "seller_id": seller_id, "verdict": "duplicate in the list: skipped"})
continue
seen.add(key)
rows.append(audit_one(session, asin, marketplace_id, seller_id, planned_was_cents, now=now))
return rows
if __name__ == "__main__":
session = new_session()
now = datetime(2026, 10, 26, tzinfo=timezone.utc) # fixed "today" so the example output never drifts
# Illustrative planned promos: (asin, marketplaceId, own sellerId, planned "was" price, landed cents)
PLANNED_PROMOS = [
("B0CL61F39H", "ATVPDKIKX0DER", "A2L77EE7U53NWQ", 44999),
("B0EXAMPLE1", "ATVPDKIKX0DER", "A2L77EE7U53NWQ", 24990),
("B0EXAMPLE2", "ATVPDKIKX0DER", "A2L77EE7U53NWQ", 1500),
]
for row in audit_batch(session, PLANNED_PROMOS, now=now):
print(row)
Output of eu_price_reduction_audit.py for one planned promo (illustrative data)
{'asin': 'B0CL61F39H', 'seller_id': 'A2L77EE7U53NWQ', 'planned_was': 44999, 'short_window': False, 'lowest_30d': 44999, 'verdict': 'PASS: proposed prior price is at or below your real 30-day low'}
{'asin': 'B0EXAMPLE1', 'seller_id': 'A2L77EE7U53NWQ', 'planned_was': 24990, 'short_window': False, 'lowest_30d': 1899, 'verdict': 'FAIL: show 1899 as the prior price, not 24990'}
{'asin': 'B0EXAMPLE2', 'seller_id': 'A2L77EE7U53NWQ', 'planned_was': 1500, 'verdict': 'no seller history here; audit manually'}

What does the verdict mean?
| Verdict | Cause | What to do |
|---|---|---|
| PASS | The planned "was" price is at or below the real 30-day low you charged | Safe to use that price as the prior price in the announcement |
| FAIL | The planned "was" price is higher than the real 30-day low you charged | Lower the announced prior price to the real low, or wait until it is true again |
| no seller history | Your seller ID has no recorded offers for this ASIN in the API | Audit from your own order/pricing records instead |
| no usable price in 30 days | Every point is outside the window, or unreadable, or missing shipping | Widen your own records search; the API alone cannot complete this audit |

Trimmed /amazon-product-statistics response: the own-seller offer history used for the audit (illustrative values)
{
"data": {
"offers": {
"A2L77EE7U53NWQ": {
"isFBA": true,
"priceHistory": [
[
"2026-09-20 10:00:00",
47999,
500
],
[
"2026-09-28 10:00:00",
44500,
499
],
[
"2026-10-05 10:00:00",
-2,
499
],
[
"2026-10-12 10:00:00",
46500,
-1
],
[
"2026-10-20 10:00:00",
46999,
0
]
]
}
},
"trackingSince": "2025-01-15"
},
"success": true
}
What does the check cost, and what should I validate first?
Every request to /amazon-product-statistics costs 1 credit, so one ASIN audit costs 1 credit and a batch of planned promos costs 1 credit per unique (asin, marketplace, seller) combination; repeats in the list are deduplicated before any request is sent. A malformed ASIN is a billed 400 ("Invalid product ASIN") on this endpoint, so the script validates the ASIN format locally first and never pays for one it can already reject. The endpoint only covers 11 specific marketplaces (.com, .co.uk, .de, .fr, .co.jp, .ca, .it, .es, .in, .com.mx, .com.br); any other marketplaceId is a free gate 400, so there is nothing to refund. A 404 is billed and not retried, 502 and 503 are billed on every attempt (up to 4), a 500 without Retry-After is refunded, and a connection timeout after retries is reported on that row instead of stopping the rest of the batch. Plans and credit prices are on the pricing page.
This is a self-audit tool, not legal advice
Article 6a sets the floor for the price shown as the reduction's reference price; it does not cover every national transposition detail or every Member State's enforcement practice. Get legal advice for your specific markets before relying on this check, and keep your own pricing records: the API's tracked history has gaps and a window shorter than 30 days for a newly listed ASIN.
What the data cannot show
- Prices before the API started tracking this ASIN or this seller's offers:
trackingSincemarks where the recorded history begins, which can be inside the 30-day window for a new listing. - The exact moment a price changed: offers are refreshed irregularly, so the recorded points are a lower bound on how often the price actually moved.
- Landed price at points where shipping is unspecified (
shippingof -1): those points are dropped rather than assumed to be free shipping. - Whether your national transposition of Article 6a adds requirements beyond the EU directive text: get legal advice for the specific markets you sell in.
- Any seller's price other than the one you request: the own-offer series is per seller ID, so auditing a marketplace you do not sell directly on yourself is not possible from this endpoint.
Frequently Asked Questions
Which API field shows the price I must display as the "was" price for an EU reduction?
None directly. Art 6a of Directive 98/6/EC needs the lowest price you, the trader, applied in the 30 days before the reduction. Compute it from your own seller offer history, data.offers[sellerId].priceHistory on /amazon-product-statistics filtered to your sellerId, not buyBoxPriceHistory.
Can I use stats.buyBoxPriceHistory for this check?
No. buyBoxPriceHistory records whichever seller held the Buy Box at each point in time, which can be a competitor, not you. Art 6a is about the price the trader itself applied, so use your own seller's priceHistory in data.offers instead.
What if my own seller ID has no tracked offer history for an ASIN?
The audit cannot run automatically: report it as unresolved and check your own sales or pricing records for the 30 days before the reduction instead of guessing.
Does Art 6a apply outside the EU?
The facts here are EU law (Directive 98/6/EC, amended by Directive 2019/2161). Rules differ by country outside the EU; get legal advice for your specific markets before relying on this check.
What does a missing shipping value in the price history mean?
shipping of -1 means the upstream data has no shippable value recorded for that point, so the landed price cannot be computed there; the script skips that point rather than guessing a shipping cost.
Bottom line: audit a planned EU price reduction against data.offers[yourSellerId].priceHistory, landed and filtered to the last 30 days, never against stats.buyBoxPriceHistory. A planned prior price at or below that real 30-day low passes; anything above it does not, and a seller ID without enough tracked history needs your own records, not a guess.